Merge pull request #1157 from gitroomhq/feat/temporal
Moving from BullMQ to Temporal - big change
This commit is contained in:
commit
95303b2975
|
|
@ -16,13 +16,10 @@ import { MediaController } from '@gitroom/backend/api/routes/media.controller';
|
|||
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';
|
||||
import { MarketplaceController } from '@gitroom/backend/api/routes/marketplace.controller';
|
||||
import { MessagesController } from '@gitroom/backend/api/routes/messages.controller';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
|
||||
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
|
||||
import { CopilotController } from '@gitroom/backend/api/routes/copilot.controller';
|
||||
import { AgenciesController } from '@gitroom/backend/api/routes/agencies.controller';
|
||||
import { PublicController } from '@gitroom/backend/api/routes/public.controller';
|
||||
import { RootController } from '@gitroom/backend/api/routes/root.controller';
|
||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||
|
|
@ -44,10 +41,7 @@ const authenticatedController = [
|
|||
MediaController,
|
||||
BillingController,
|
||||
NotificationsController,
|
||||
MarketplaceController,
|
||||
MessagesController,
|
||||
CopilotController,
|
||||
AgenciesController,
|
||||
WebhookController,
|
||||
SignatureController,
|
||||
AutopostController,
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.agency.dto';
|
||||
|
||||
@ApiTags('Agencies')
|
||||
@Controller('/agencies')
|
||||
export class AgenciesController {
|
||||
constructor(private _agenciesService: AgenciesService) {}
|
||||
@Get('/')
|
||||
async getAgencyByUser(@GetUserFromRequest() user: User) {
|
||||
return (await this._agenciesService.getAgencyByUser(user)) || {};
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
async createAgency(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: CreateAgencyDto
|
||||
) {
|
||||
return this._agenciesService.createAgency(user, body);
|
||||
}
|
||||
|
||||
@Post('/action/:action/:id')
|
||||
async updateAgency(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Param('action') action: string,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
if (!user.isSuperAdmin) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
return this._agenciesService.approveOrDecline(user.email, action, id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +1,13 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
|
||||
@ApiTags('Analytics')
|
||||
@Controller('/analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
@Get('/')
|
||||
async getStars(@GetOrgFromRequest() org: Organization) {
|
||||
return this._starsService.getStars(org.id);
|
||||
}
|
||||
|
||||
@Get('/trending')
|
||||
async getTrending() {
|
||||
const todayTrending = dayjs(dayjs().format('YYYY-MM-DDT12:00:00'));
|
||||
const last = todayTrending.isAfter(dayjs())
|
||||
? todayTrending.subtract(1, 'day')
|
||||
: todayTrending;
|
||||
const nextTrending = last.add(1, 'day');
|
||||
|
||||
return {
|
||||
last: last.format('YYYY-MM-DD HH:mm:ss'),
|
||||
predictions: nextTrending.format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/stars')
|
||||
async getStarsFilter(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() starsFilter: StarsListDto
|
||||
) {
|
||||
return {
|
||||
stars: await this._starsService.getStarsFilter(org.id, starsFilter),
|
||||
};
|
||||
}
|
||||
constructor(private _integrationService: IntegrationService) {}
|
||||
|
||||
@Get('/:integration')
|
||||
async getIntegration(
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.service';
|
||||
import { AddRemoveItemDto } from '@gitroom/nestjs-libraries/dtos/marketplace/add.remove.item.dto';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { ChangeActiveDto } from '@gitroom/nestjs-libraries/dtos/marketplace/change.active.dto';
|
||||
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { AudienceDto } from '@gitroom/nestjs-libraries/dtos/marketplace/audience.dto';
|
||||
import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
@ApiTags('Marketplace')
|
||||
@Controller('/marketplace')
|
||||
export class MarketplaceController {
|
||||
constructor(
|
||||
private _itemUserService: ItemUserService,
|
||||
private _stripeService: StripeService,
|
||||
private _userService: UsersService,
|
||||
private _messagesService: MessagesService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
getInfluencers(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: ItemsDto
|
||||
) {
|
||||
return this._userService.getMarketplacePeople(
|
||||
organization.id,
|
||||
user.id,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/conversation')
|
||||
createConversation(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Body() body: NewConversationDto
|
||||
) {
|
||||
return this._messagesService.createConversation(
|
||||
user.id,
|
||||
organization.id,
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/bank')
|
||||
connectBankAccount(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Query('country') country: string
|
||||
) {
|
||||
return this._stripeService.createAccountProcess(
|
||||
user.id,
|
||||
user.email,
|
||||
country
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/item')
|
||||
async addItems(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: AddRemoveItemDto
|
||||
) {
|
||||
return this._itemUserService.addOrRemoveItem(body.state, user.id, body.key);
|
||||
}
|
||||
|
||||
@Post('/active')
|
||||
async changeActive(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: ChangeActiveDto
|
||||
) {
|
||||
await this._userService.changeMarketplaceActive(user.id, body.active);
|
||||
}
|
||||
|
||||
@Post('/audience')
|
||||
async changeAudience(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: AudienceDto
|
||||
) {
|
||||
await this._userService.changeAudienceSize(user.id, body.audience);
|
||||
}
|
||||
|
||||
@Get('/item')
|
||||
async getItems(@GetUserFromRequest() user: User) {
|
||||
return this._itemUserService.getItems(user.id);
|
||||
}
|
||||
|
||||
@Get('/orders')
|
||||
async getOrders(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Query('type') type: 'seller' | 'buyer'
|
||||
) {
|
||||
return this._messagesService.getOrders(user.id, organization.id, type);
|
||||
}
|
||||
|
||||
@Get('/account')
|
||||
async getAccount(@GetUserFromRequest() user: User) {
|
||||
const { account, marketplace, connectedAccount, name, picture, audience } =
|
||||
await this._userService.getUserByEmail(user.email);
|
||||
return {
|
||||
account,
|
||||
marketplace,
|
||||
connectedAccount,
|
||||
fullname: name,
|
||||
audience,
|
||||
picture,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/offer')
|
||||
async createOffer(
|
||||
@GetUserFromRequest() user: User,
|
||||
@Body() body: CreateOfferDto
|
||||
) {
|
||||
return this._messagesService.createOffer(user.id, body);
|
||||
}
|
||||
|
||||
@Get('/posts/:id')
|
||||
async post(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const getPost = await this._messagesService.getPost(
|
||||
user.id,
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
if (!getPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...(await this._postsService.getPost(getPost.organizationId, id)),
|
||||
providerId: getPost.integration.providerIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/posts/:id/revision')
|
||||
async revision(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('message') message: string
|
||||
) {
|
||||
return this._messagesService.requestRevision(
|
||||
user.id,
|
||||
organization.id,
|
||||
id,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/posts/:id/approve')
|
||||
async approve(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('message') message: string
|
||||
) {
|
||||
return this._messagesService.requestApproved(
|
||||
user.id,
|
||||
organization.id,
|
||||
id,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/posts/:id/cancel')
|
||||
async cancel(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._messagesService.requestCancel(organization.id, id);
|
||||
}
|
||||
|
||||
@Post('/offer/:id/complete')
|
||||
async completeOrder(
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const order = await this._messagesService.completeOrderAndPay(
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
|
||||
if (!order) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._stripeService.payout(
|
||||
id,
|
||||
order.charge,
|
||||
order.account,
|
||||
order.price
|
||||
);
|
||||
} catch (e) {
|
||||
await this._messagesService.payoutProblem(
|
||||
id,
|
||||
order.sellerId,
|
||||
order.price
|
||||
);
|
||||
}
|
||||
await this._messagesService.completeOrder(id);
|
||||
}
|
||||
|
||||
@Post('/orders/:id/payment')
|
||||
async payOrder(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
const orderDetails = await this._messagesService.getOrderDetails(
|
||||
user.id,
|
||||
organization.id,
|
||||
id
|
||||
);
|
||||
const payment = await this._stripeService.payAccountStepOne(
|
||||
user.id,
|
||||
organization,
|
||||
orderDetails.seller,
|
||||
orderDetails.order.id,
|
||||
orderDetails.order.ordersItems.map((p) => ({
|
||||
quantity: p.quantity,
|
||||
integrationType: p.integration.providerIdentifier,
|
||||
price: p.price,
|
||||
})),
|
||||
orderDetails.order.messageGroupId
|
||||
);
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
||||
@ApiTags('Messages')
|
||||
@Controller('/messages')
|
||||
export class MessagesController {
|
||||
constructor(private _messagesService: MessagesService) {}
|
||||
|
||||
@Get('/')
|
||||
getMessagesGroup(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization
|
||||
) {
|
||||
return this._messagesService.getMessagesGroup(user.id, organization.id);
|
||||
}
|
||||
|
||||
@Get('/:groupId/:page')
|
||||
getMessages(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('groupId') groupId: string,
|
||||
@Param('page') page: string
|
||||
) {
|
||||
return this._messagesService.getMessages(
|
||||
user.id,
|
||||
organization.id,
|
||||
groupId,
|
||||
+page
|
||||
);
|
||||
}
|
||||
@Post('/:groupId')
|
||||
createMessage(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Param('groupId') groupId: string,
|
||||
@Body() message: AddMessageDto
|
||||
) {
|
||||
return this._messagesService.createMessage(
|
||||
user.id,
|
||||
organization.id,
|
||||
groupId,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,14 @@
|
|||
import { Controller, Get, HttpException, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@ApiTags('Monitor')
|
||||
@Controller('/monitor')
|
||||
export class MonitorController {
|
||||
constructor(private _workerServiceProducer: BullMqClient) {}
|
||||
|
||||
@Get('/queue/:name')
|
||||
async getMessagesGroup(@Param('name') name: string) {
|
||||
const { valid } =
|
||||
await this._workerServiceProducer.checkForStuckWaitingJobs(name);
|
||||
|
||||
if (valid) {
|
||||
return {
|
||||
status: 'success',
|
||||
message: `Queue ${name} is healthy.`,
|
||||
};
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
status: 'error',
|
||||
message: `Queue ${name} has stuck waiting jobs.`,
|
||||
},
|
||||
503
|
||||
);
|
||||
return {
|
||||
status: 'success',
|
||||
message: `Queue ${name} is healthy.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization, User } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
|
|
@ -31,8 +29,6 @@ import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/p
|
|||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _starsService: StarsService,
|
||||
private _messagesService: MessagesService,
|
||||
private _agentGraphService: AgentGraphService,
|
||||
private _shortLinkService: ShortLinkService
|
||||
) {}
|
||||
|
|
@ -50,14 +46,6 @@ export class PostsController {
|
|||
return { ask: this._shortLinkService.askShortLinkedin(body.messages) };
|
||||
}
|
||||
|
||||
@Get('/marketplace/:id')
|
||||
async getMarketplacePosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._messagesService.getMarketplaceAvailableOffers(org.id, id);
|
||||
}
|
||||
|
||||
@Post('/:id/comments')
|
||||
async createComment(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
@ -115,11 +103,6 @@ export class PostsController {
|
|||
return { date: await this._postsService.findFreeDateTime(org.id, id) };
|
||||
}
|
||||
|
||||
@Get('/predict-trending')
|
||||
predictTrending() {
|
||||
return this._starsService.predictTrending();
|
||||
}
|
||||
|
||||
@Get('/old')
|
||||
oldPosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
import { Organization } from '@prisma/client';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto';
|
||||
|
|
@ -12,95 +11,9 @@ import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/p
|
|||
@Controller('/settings')
|
||||
export class SettingsController {
|
||||
constructor(
|
||||
private _starsService: StarsService,
|
||||
private _organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
@Get('/github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getConnectedGithubAccounts(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
github: (
|
||||
await this._starsService.getGitHubRepositoriesByOrgId(org.id)
|
||||
).map((repo) => ({
|
||||
id: repo.id,
|
||||
login: repo.login,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async addGitHub(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body('code') code: string
|
||||
) {
|
||||
if (!code) {
|
||||
throw new Error('No code provided');
|
||||
}
|
||||
await this._starsService.addGitHub(org.id, code);
|
||||
}
|
||||
|
||||
@Get('/github/url')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
authUrl() {
|
||||
return {
|
||||
url: `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(
|
||||
'user:email'
|
||||
)}&redirect_uri=${encodeURIComponent(
|
||||
`${process.env.FRONTEND_URL}/settings`
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/organizations/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getOrganizations(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return {
|
||||
organizations: await this._starsService.getOrganizations(org.id, id),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/organizations/:id/:github')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async getRepositories(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Param('github') github: string
|
||||
) {
|
||||
return {
|
||||
repositories: await this._starsService.getRepositoriesOfOrganization(
|
||||
org.id,
|
||||
id,
|
||||
github
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/organizations/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async updateGitHubLogin(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string,
|
||||
@Body('login') login: string
|
||||
) {
|
||||
return this._starsService.updateGitHubLogin(org.id, id, login);
|
||||
}
|
||||
|
||||
@Delete('/repository/:id')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.ADMIN])
|
||||
async deleteRepository(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Param('id') id: string
|
||||
) {
|
||||
return this._starsService.deleteRepository(org.id, id);
|
||||
}
|
||||
|
||||
@Get('/team')
|
||||
@CheckPolicies(
|
||||
[AuthorizationActions.Create, Sections.TEAM_MEMBERS],
|
||||
|
|
|
|||
|
|
@ -1,47 +1,19 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Header,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
RawBodyRequest,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
|
||||
|
||||
@ApiTags('Stripe')
|
||||
@Controller('/stripe')
|
||||
export class StripeController {
|
||||
constructor(
|
||||
private readonly _stripeService: StripeService,
|
||||
private readonly _codesService: CodesService
|
||||
) {}
|
||||
@Post('/connect')
|
||||
stripeConnect(@Req() req: RawBodyRequest<Request>) {
|
||||
const event = this._stripeService.validateRequest(
|
||||
req.rawBody,
|
||||
// @ts-ignore
|
||||
req.headers['stripe-signature'],
|
||||
process.env.STRIPE_SIGNING_KEY_CONNECT
|
||||
);
|
||||
|
||||
// Maybe it comes from another stripe webhook
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (event?.data?.object?.metadata?.service !== 'gitroom') {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
default:
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
stripe(@Req() req: RawBodyRequest<Request>) {
|
||||
|
|
@ -66,8 +38,6 @@ export class StripeController {
|
|||
switch (event.type) {
|
||||
case 'invoice.payment_succeeded':
|
||||
return this._stripeService.paymentSucceeded(event);
|
||||
case 'account.updated':
|
||||
return this._stripeService.updateAccount(event);
|
||||
case 'customer.subscription.created':
|
||||
return this._stripeService.createSubscription(event);
|
||||
case 'customer.subscription.updated':
|
||||
|
|
@ -81,11 +51,4 @@ export class StripeController {
|
|||
throw new HttpException(e, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/lifetime-deal-codes/:provider')
|
||||
@Header('Content-disposition', 'attachment; filename=codes.csv')
|
||||
@Header('Content-type', 'text/csv')
|
||||
async getStripeCodes(@Param('provider') providerToken: string) {
|
||||
return this._codesService.generateCodes(providerToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/databa
|
|||
import { ApiModule } from '@gitroom/backend/api/api.module';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
|
||||
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
|
|
@ -13,12 +12,14 @@ import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module';
|
|||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
||||
import { getTemporalModule } from '@gitroom/nestjs-libraries/temporal/temporal.module';
|
||||
import { TemporalRegisterMissingSearchAttributesModule } from '@gitroom/nestjs-libraries/temporal/temporal.register';
|
||||
import { InfiniteWorkflowRegisterModule } from '@gitroom/nestjs-libraries/temporal/infinite.workflow.register';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PublicApiModule,
|
||||
|
|
@ -26,6 +27,9 @@ import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
|||
ThirdPartyModule,
|
||||
VideoModule,
|
||||
ChatModule,
|
||||
getTemporalModule(false),
|
||||
TemporalRegisterMissingSearchAttributesModule,
|
||||
InfiniteWorkflowRegisterModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
|
|
@ -46,7 +50,6 @@ import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module';
|
|||
},
|
||||
],
|
||||
exports: [
|
||||
BullMqModule,
|
||||
DatabaseModule,
|
||||
ApiModule,
|
||||
PublicApiModule,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ initializeSentry('backend', true);
|
|||
|
||||
import { loadSwagger } from '@gitroom/helpers/swagger/load.swagger';
|
||||
import { json } from 'express';
|
||||
import { Runtime } from '@temporalio/worker';
|
||||
Runtime.install({ shutdownSignals: [] });
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
|
|
@ -21,7 +23,11 @@ async function start() {
|
|||
rawBody: true,
|
||||
cors: {
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-copilotkit-runtime-client-gql-version'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'x-copilotkit-runtime-client-gql-version',
|
||||
],
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CommandModule as ExternalCommandModule } from 'nestjs-command';
|
||||
import { CheckStars } from './tasks/check.stars';
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { RefreshTokens } from './tasks/refresh.tokens';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { ConfigurationTask } from './tasks/configuration';
|
||||
import { AgentRun } from './tasks/agent.run';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Module({
|
||||
imports: [ExternalCommandModule, DatabaseModule, BullMqModule, AgentModule],
|
||||
imports: [ExternalCommandModule, DatabaseModule, AgentModule],
|
||||
controllers: [],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask, AgentRun],
|
||||
providers: [RefreshTokens, ConfigurationTask, AgentRun],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import { Command, Positional } from 'nestjs-command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class CheckStars {
|
||||
constructor(private _workerServiceProducer: BullMqClient) {}
|
||||
@Command({
|
||||
command: 'sync:stars <login>',
|
||||
describe: 'Sync stars for a login',
|
||||
})
|
||||
async create(
|
||||
@Positional({
|
||||
name: 'login',
|
||||
describe: 'login {owner}/{repo}',
|
||||
type: 'string',
|
||||
})
|
||||
login: string
|
||||
) {
|
||||
this._workerServiceProducer
|
||||
.emit('check_stars', { payload: { login } })
|
||||
.subscribe();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Command({
|
||||
command: 'sync:all_stars <login>',
|
||||
describe: 'Sync all stars for a login',
|
||||
})
|
||||
async syncAllStars(
|
||||
@Positional({
|
||||
name: 'login',
|
||||
describe: 'login {owner}/{repo}',
|
||||
type: 'string',
|
||||
})
|
||||
login: string
|
||||
) {
|
||||
this._workerServiceProducer
|
||||
.emit('sync_all_stars', { payload: { login } })
|
||||
.subscribe();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Command({
|
||||
command: 'sync:trending',
|
||||
describe: 'Sync trending',
|
||||
})
|
||||
async syncTrending() {
|
||||
this._workerServiceProducer.emit('sync_trending', {}).subscribe();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
import { CheckMissingQueues } from '@gitroom/cron/tasks/check.missing.queues';
|
||||
import { PostNowPendingQueues } from '@gitroom/cron/tasks/post.now.pending.queues';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
DatabaseModule,
|
||||
ScheduleModule.forRoot(),
|
||||
BullMqModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [FILTER, CheckMissingQueues, PostNowPendingQueues],
|
||||
})
|
||||
export class CronModule {}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { initializeSentry } from '@gitroom/nestjs-libraries/sentry/initialize.sentry';
|
||||
initializeSentry('cron');
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CronModule } from './cron.module';
|
||||
|
||||
async function bootstrap() {
|
||||
// some comment again
|
||||
await NestFactory.createApplicationContext(CronModule);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class CheckMissingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('0 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.searchForMissingThreeHoursPosts();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: dayjs(job.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class PostNowPendingQueues {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
@Cron('*/16 * * * *')
|
||||
async handleCron() {
|
||||
const list = await this._postService.checkPending15minutesBack();
|
||||
const notExists = (
|
||||
await Promise.all(
|
||||
list.map(async (p) => ({
|
||||
id: p.id,
|
||||
publishDate: p.publishDate,
|
||||
isJob:
|
||||
['delayed', 'waiting'].indexOf(
|
||||
await this._workerServiceProducer
|
||||
.getQueue('post')
|
||||
.getJobState(p.id)
|
||||
) > -1,
|
||||
}))
|
||||
)
|
||||
).filter((p) => !p.isJob);
|
||||
|
||||
for (const job of notExists) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: job.id,
|
||||
options: {
|
||||
delay: 0,
|
||||
},
|
||||
payload: {
|
||||
id: job.id,
|
||||
delay: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noLib": false,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@ export const SettingsPopup: FC<{
|
|||
return;
|
||||
}
|
||||
toast.show(t('profile_updated', 'Profile updated'));
|
||||
swr.mutate('/marketplace/account');
|
||||
close();
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const BuyerSeller: FC = () => {
|
||||
const path = usePathname();
|
||||
const t = useT();
|
||||
const pathComputed = path === '/marketplace' ? '/marketplace/seller' : path;
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="w-[286px] h-[50px] bg-third p-[9px] flex select-none absolute -translate-y-[63px] end-0">
|
||||
<div className="bg-input flex flex-1">
|
||||
<Link
|
||||
href="/marketplace/seller"
|
||||
className={clsx(
|
||||
'flex justify-center items-center flex-1',
|
||||
pathComputed.indexOf('/marketplace/seller') > -1 &&
|
||||
'bg-forth text-white'
|
||||
)}
|
||||
>
|
||||
{t('seller', 'Seller')}
|
||||
</Link>
|
||||
<Link
|
||||
href="/marketplace/buyer"
|
||||
className={clsx(
|
||||
'flex justify-center items-center flex-1',
|
||||
pathComputed.indexOf('/marketplace/buyer') > -1 &&
|
||||
'bg-forth text-white'
|
||||
)}
|
||||
>
|
||||
{t('buyer', 'Buyer')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
FC,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Checkbox } from '@gitroom/react/form/checkbox';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import {
|
||||
allTagsOptions,
|
||||
tagsList,
|
||||
} from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
|
||||
import { capitalize, chunk, fill } from 'lodash';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
|
||||
import { OrderList } from '@gitroom/frontend/components/marketplace/order.list';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export interface Root {
|
||||
list: List[];
|
||||
count: number;
|
||||
}
|
||||
export interface List {
|
||||
id: string;
|
||||
name: any;
|
||||
bio: string;
|
||||
audience: number;
|
||||
picture: {
|
||||
id: string;
|
||||
path: string;
|
||||
};
|
||||
organizations: Organization[];
|
||||
items: Item[];
|
||||
}
|
||||
export interface Organization {
|
||||
organization: Organization2;
|
||||
}
|
||||
export interface Organization2 {
|
||||
Integration: Integration[];
|
||||
}
|
||||
export interface Integration {
|
||||
providerIdentifier: string;
|
||||
}
|
||||
export interface Item {
|
||||
key: string;
|
||||
}
|
||||
export const LabelCheckbox: FC<{
|
||||
label: string;
|
||||
name: string;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
onChange: (value: string, status: boolean) => void;
|
||||
}> = (props) => {
|
||||
const { label, name, value, checked, onChange } = props;
|
||||
const ref = useRef<any>(null);
|
||||
const [innerCheck, setInnerCheck] = useState(checked);
|
||||
const change = useCallback(() => {
|
||||
setInnerCheck(!innerCheck);
|
||||
onChange(value, !innerCheck);
|
||||
}, [innerCheck]);
|
||||
return (
|
||||
<div className="flex items-center gap-[10px] select-none">
|
||||
<Checkbox
|
||||
ref={ref}
|
||||
variant="hollow"
|
||||
name={name}
|
||||
checked={checked}
|
||||
onChange={change}
|
||||
disableForm={true}
|
||||
/>
|
||||
<label
|
||||
onClick={() => ref.current.click()}
|
||||
className="text-[20px]"
|
||||
htmlFor={name}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Pagination: FC<{
|
||||
results: number;
|
||||
}> = (props) => {
|
||||
const { results } = props;
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const page = +(parseInt(search.get('page')!) || 1) - 1;
|
||||
const t = useT();
|
||||
const from = page * 8;
|
||||
const to = (page + 1) * 8;
|
||||
const pagesArray = useMemo(() => {
|
||||
return Array.from(
|
||||
{
|
||||
length: Math.ceil(results / 8),
|
||||
},
|
||||
(_, i) => i + 1
|
||||
);
|
||||
}, [results]);
|
||||
const changePage = useCallback(
|
||||
(newPage: number) => () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('page', String(newPage));
|
||||
router.replace('?' + params.toString(), {
|
||||
scroll: true,
|
||||
});
|
||||
},
|
||||
[page]
|
||||
);
|
||||
if (results < 8) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center relative">
|
||||
<div className="absolute start-0">
|
||||
{t('showing', 'Showing')}
|
||||
{from + 1}
|
||||
{t('to', 'to')}
|
||||
{to > results ? results : to}
|
||||
{t('from', 'from')}
|
||||
{results}
|
||||
{t('results', 'Results')}
|
||||
</div>
|
||||
<div className="flex mx-auto">
|
||||
{page > 0 && (
|
||||
<div>
|
||||
<svg
|
||||
width="41"
|
||||
height="40"
|
||||
viewBox="0 0 41 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={changePage(page)}
|
||||
>
|
||||
<g clipPath="url(#clip0_703_22324)">
|
||||
<path
|
||||
d="M22.5 25L17.5 20L22.5 15"
|
||||
stroke="#64748B"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_703_22324">
|
||||
<rect x="0.5" width="40" height="40" rx="8" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{pagesArray.map((p) => (
|
||||
<div
|
||||
key={p}
|
||||
onClick={changePage(p)}
|
||||
className={clsx(
|
||||
'w-[40px] h-[40px] flex justify-center items-center rounded-[8px] cursor-pointer',
|
||||
p === page + 1 ? 'bg-customColor4' : 'text-inputText'
|
||||
)}
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
))}
|
||||
{page + 1 < pagesArray[pagesArray.length - 1] && (
|
||||
<svg
|
||||
onClick={changePage(page + 2)}
|
||||
width="41"
|
||||
height="40"
|
||||
viewBox="0 0 41 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.5 15L23.5 20L18.5 25"
|
||||
stroke="#64748B"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Options: FC<{
|
||||
title: string;
|
||||
options: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
}>;
|
||||
onChange?: (key: string, value: boolean) => void;
|
||||
preSelected?: string[];
|
||||
rows?: number;
|
||||
search: boolean;
|
||||
}> = (props) => {
|
||||
const { title, onChange, search, preSelected } = props;
|
||||
const query = 'services';
|
||||
const [selected, setPreSelected] = useState<string[]>(
|
||||
preSelected?.slice(0) || []
|
||||
);
|
||||
const rows = props.rows || 1;
|
||||
const optionsGroupList = chunk(
|
||||
props.options,
|
||||
Math.ceil(props.options.length / rows)
|
||||
);
|
||||
const optionsGroup =
|
||||
optionsGroupList.length < rows
|
||||
? [
|
||||
...optionsGroupList,
|
||||
...fill(Array(rows - optionsGroupList.length), []),
|
||||
]
|
||||
: optionsGroupList;
|
||||
const router = useRouter();
|
||||
const searchParams = (useSearchParams().get(query) || '')?.split(',') || [];
|
||||
const change = (value: string, state: boolean) => {
|
||||
if (onChange) {
|
||||
onChange(value, state);
|
||||
}
|
||||
if (!search) {
|
||||
return;
|
||||
}
|
||||
const getAll = new URLSearchParams(window.location.search).get(query);
|
||||
const splitAll = (getAll?.split(',') || []).filter((f) => f);
|
||||
if (state) {
|
||||
splitAll?.push(value);
|
||||
} else {
|
||||
splitAll?.splice(splitAll.indexOf(value), 1);
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (!splitAll?.length) {
|
||||
params.delete(query);
|
||||
} else {
|
||||
params.set(query, splitAll?.join(',') || '');
|
||||
}
|
||||
router.replace('?' + params.toString());
|
||||
return params.toString();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="h-[56px] text-[20px] font-[600] flex items-center px-[24px] bg-customColor8">
|
||||
{title}
|
||||
</div>
|
||||
<div className="bg-customColor3 flex px-[32px] py-[24px]">
|
||||
{optionsGroup.map((options, key) => (
|
||||
<div
|
||||
key={`options_` + key}
|
||||
className="flex gap-[16px] flex-col flex-1 justify-start"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={option.key} className="flex gap-[10px]">
|
||||
<LabelCheckbox
|
||||
value={option.key}
|
||||
label={option.value}
|
||||
checked={
|
||||
selected?.indexOf(option.key) > -1 ||
|
||||
searchParams.indexOf(option.key) > -1
|
||||
}
|
||||
name={query}
|
||||
onChange={change}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const RequestService: FC<{
|
||||
toId: string;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { toId, name } = props;
|
||||
const router = useRouter();
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(NewConversationDto);
|
||||
}, []);
|
||||
const form = useForm({
|
||||
resolver,
|
||||
values: {
|
||||
to: toId,
|
||||
message: '',
|
||||
},
|
||||
});
|
||||
const close = useCallback(() => {
|
||||
return modal.closeAll();
|
||||
}, []);
|
||||
|
||||
const t = useT();
|
||||
|
||||
const createConversation: SubmitHandler<NewConversationDto> = useCallback(
|
||||
async (data) => {
|
||||
const { id } = await (
|
||||
await fetch('/marketplace/conversation', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
).json();
|
||||
close();
|
||||
router.push(`/messages/${id}`);
|
||||
},
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(createConversation)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="w-full max-w-[920px] mx-auto bg-sixth px-[16px] rounded-[4px] border border-customColor6 gap-[24px] flex flex-col relative">
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute end-[20px] top-[20px] 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="text-[18px] font-[500] flex flex-col">
|
||||
<TopTitle title={`Send a message to ${name}`} />
|
||||
<Textarea
|
||||
placeholder="Add a message like: I'm intrested in 3 posts for Linkedin... (min 50 chars)"
|
||||
className="mt-[14px] resize-none h-[400px]"
|
||||
name="message"
|
||||
label=""
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={!form.formState.isValid}
|
||||
type="submit"
|
||||
className="w-[144px] mb-[16px] rounded-[4px] text-[14px]"
|
||||
>
|
||||
{t('send_message', 'Send Message')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export const Card: FC<{
|
||||
data: List;
|
||||
}> = (props) => {
|
||||
const { data } = props;
|
||||
const modal = useModals();
|
||||
const tags = useMemo(() => {
|
||||
return data.items
|
||||
.filter((f) => !['content-writer', 'influencers'].includes(f.key))
|
||||
.map((p) => {
|
||||
return allTagsOptions?.find((t) => t.key === p.key)?.value;
|
||||
});
|
||||
}, [data]);
|
||||
const requestService = useCallback(() => {
|
||||
modal.openModal({
|
||||
children: <RequestService toId={data.id} name={data.name || 'Noname'} />,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
withCloseButton: false,
|
||||
size: '100%',
|
||||
});
|
||||
}, []);
|
||||
const t = useT();
|
||||
|
||||
const identifier = useMemo(() => {
|
||||
return [
|
||||
...new Set(
|
||||
data.organizations.flatMap((p) =>
|
||||
p.organization.Integration.flatMap((d) => d.providerIdentifier)
|
||||
)
|
||||
),
|
||||
];
|
||||
}, []);
|
||||
return (
|
||||
<div className="min-h-[155px] bg-sixth p-[24px] flex">
|
||||
<div className="flex gap-[16px] flex-1">
|
||||
<div>
|
||||
<div className="h-[103px] w-[103px] bg-red-500/10 rounded-full relative">
|
||||
{data?.picture?.path && (
|
||||
<img
|
||||
src={data?.picture?.path}
|
||||
className="rounded-full w-full h-full"
|
||||
/>
|
||||
)}
|
||||
<div className="w-[80px] h-[28px] bg-customColor4 absolute bottom-0 start-[50%] -translate-x-[50%] rounded-[30px] flex gap-[4px] justify-center items-center">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7.82348 9.80876C8.45164 9.28076 8.90221 8.57235 9.11409 7.77958C9.32597 6.98682 9.28891 6.14807 9.00792 5.37709C8.72694 4.60611 8.21563 3.9402 7.54334 3.46967C6.87106 2.99914 6.07032 2.74677 5.24973 2.74677C4.42914 2.74677 3.62841 2.99914 2.95612 3.46967C2.28383 3.9402 1.77253 4.60611 1.49154 5.37709C1.21056 6.14807 1.17349 6.98682 1.38537 7.77958C1.59725 8.57235 2.04782 9.28076 2.67598 9.80876C1.69509 10.2523 0.845054 10.9411 0.207856 11.8088C0.0901665 11.9691 0.0410049 12.1697 0.0711866 12.3663C0.101368 12.5629 0.208421 12.7395 0.368794 12.8572C0.529167 12.9749 0.729724 13.024 0.926344 12.9939C1.12296 12.9637 1.29954 12.8566 1.41723 12.6963C1.85832 12.0938 2.43521 11.6039 3.10109 11.2662C3.76697 10.9284 4.50309 10.7524 5.24973 10.7524C5.99637 10.7524 6.73249 10.9284 7.39837 11.2662C8.06426 11.6039 8.64114 12.0938 9.08223 12.6963C9.19992 12.8567 9.37653 12.9638 9.57321 12.9941C9.76989 13.0243 9.97053 12.9752 10.131 12.8575C10.2914 12.7398 10.3986 12.5632 10.4288 12.3665C10.459 12.1699 10.4099 11.9692 10.2922 11.8088C9.65465 10.9412 8.80445 10.2524 7.82348 9.80876ZM2.74973 6.75001C2.74973 6.25556 2.89635 5.77221 3.17106 5.36108C3.44576 4.94996 3.83621 4.62953 4.29302 4.44031C4.74984 4.25109 5.2525 4.20158 5.73746 4.29805C6.22241 4.39451 6.66787 4.63261 7.0175 4.98224C7.36713 5.33187 7.60523 5.77733 7.70169 6.26228C7.79816 6.74724 7.74865 7.2499 7.55943 7.70672C7.37021 8.16353 7.04978 8.55398 6.63866 8.82868C6.22753 9.10339 5.74418 9.25001 5.24973 9.25001C4.58669 9.25001 3.95081 8.98662 3.48196 8.51778C3.01312 8.04894 2.74973 7.41305 2.74973 6.75001ZM15.631 12.8544C15.5516 12.9127 15.4615 12.9549 15.3658 12.9784C15.2701 13.0019 15.1707 13.0063 15.0733 12.9914C14.9759 12.9765 14.8824 12.9425 14.7982 12.8914C14.7139 12.8404 14.6405 12.7732 14.5822 12.6938C14.1401 12.0925 13.5629 11.6034 12.8973 11.2658C12.2317 10.9282 11.4961 10.7515 10.7497 10.75C10.5508 10.75 10.3601 10.671 10.2194 10.5303C10.0787 10.3897 9.99973 10.1989 9.99973 10C9.99973 9.8011 10.0787 9.61033 10.2194 9.46968C10.3601 9.32903 10.5508 9.25001 10.7497 9.25001C11.1178 9.24958 11.4813 9.16786 11.8142 9.01071C12.147 8.85355 12.4411 8.62482 12.6753 8.34086C12.9096 8.05691 13.0782 7.72473 13.1692 7.36805C13.2602 7.01138 13.2713 6.63901 13.2017 6.27754C13.1322 5.91607 12.9837 5.57443 12.7668 5.277C12.5499 4.97958 12.27 4.73373 11.9471 4.557C11.6242 4.38027 11.2662 4.27702 10.8988 4.25464C10.5314 4.23225 10.1636 4.29128 9.82161 4.42751C9.73 4.46502 9.63188 4.48402 9.5329 4.48343C9.43392 4.48283 9.33603 4.46265 9.24488 4.42404C9.15374 4.38543 9.07114 4.32916 9.00184 4.25848C8.93255 4.18779 8.87793 4.10409 8.84114 4.0122C8.80435 3.9203 8.78612 3.82204 8.78749 3.72306C8.78885 3.62409 8.8098 3.52636 8.84912 3.43552C8.88844 3.34468 8.94535 3.26252 9.01658 3.19378C9.0878 3.12504 9.17193 3.07108 9.26411 3.03501C10.1468 2.6816 11.1265 2.65418 12.0276 2.95767C12.9287 3.26115 13.6922 3.87569 14.1812 4.6911C14.6703 5.50652 14.8528 6.46947 14.6962 7.4073C14.5396 8.34512 14.0541 9.1965 13.3266 9.80876C14.3075 10.2523 15.1575 10.9411 15.7947 11.8088C15.9113 11.9693 15.9594 12.1694 15.9288 12.3654C15.8981 12.5613 15.791 12.7372 15.631 12.8544Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[14px]">{data?.audience}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[8px]">
|
||||
<div className="flex gap-[14px] items-center">
|
||||
<div className="text-[24px]">{data.name || 'Noname'}</div>
|
||||
<div className="flex gap-[3px]">
|
||||
{data.items.some((i) => i.key === 'content-writer') && (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-customColor6 rounded-[34px] py-[8px] px-[12px] text-[12px]',
|
||||
)}
|
||||
>
|
||||
{t('content_writer', 'Content Writer')}
|
||||
</div>
|
||||
)}
|
||||
{data.items.some((i) => i.key === 'influencers') && (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-customColor6 rounded-[34px] py-[8px] px-[12px] text-[12px]',
|
||||
)}
|
||||
>
|
||||
{t('influencer', 'Influencer')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-[10px]">
|
||||
{identifier.map((i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={`/icons/platforms/${i}.png`}
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[18px] text-customColor18 font-[400]">
|
||||
{data.bio || 'No bio'}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'gap-[8px] flex items-center text-[10px] font-[300] text-customColor41 tracking-[1.2px] uppercase',
|
||||
)}
|
||||
>
|
||||
{tags.map((tag, index) => (
|
||||
<Fragment key={tag}>
|
||||
<div>{tag}</div>
|
||||
{index !== tags.length - 1 && (
|
||||
<div>
|
||||
<div className="w-[4px] h-[4px] bg-customColor1 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-[100px] items-center flex">
|
||||
<Button onClick={requestService}>
|
||||
{t('request_service', 'Request Service')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Buyer = () => {
|
||||
const search = useSearchParams();
|
||||
const services = search.get('services');
|
||||
const page = +(search.get('page') || 1);
|
||||
const router = useRouter();
|
||||
const fetch = useFetch();
|
||||
const marketplace = useCallback(async () => {
|
||||
return await (
|
||||
await fetch('/marketplace', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
items: services?.split(',').filter((f) => f) || [],
|
||||
page: page === 0 ? 1 : page,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
}, [services, page]);
|
||||
|
||||
const t = useT();
|
||||
|
||||
useEffect(() => {
|
||||
if (!services) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('page', '1');
|
||||
router.replace('?' + params.toString());
|
||||
}, [services]);
|
||||
const { data: list } = useSWR<Root>('search' + services + page, marketplace);
|
||||
return (
|
||||
<div className="flex flex-col items-center mt-[100px] gap-[27px] text-center">
|
||||
<div>
|
||||
<img src="/peoplemarketplace.svg" />
|
||||
</div>
|
||||
<div className="text-[48px]">
|
||||
{t(
|
||||
'the_marketplace_is_not_opened_yet',
|
||||
'The marketplace is not opened yet'
|
||||
)}
|
||||
<br />
|
||||
{t('check_again_soon', 'Check again soon!')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<OrderList type="buyer" />
|
||||
</div>
|
||||
<div className="flex mt-[29px] w-full gap-[43px]">
|
||||
<div className="w-[330px]">
|
||||
<div className="flex flex-col gap-[16px]">
|
||||
<h2 className="text-[20px]">{t('filter', 'Filter')}</h2>
|
||||
<div className="flex flex-col">
|
||||
{tagsList.map((tag) => (
|
||||
<Options
|
||||
search={true}
|
||||
key={tag.key}
|
||||
options={tag.options}
|
||||
title={tag.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 gap-[16px] flex-col flex">
|
||||
<div className="text-[20px] text-right">
|
||||
{list?.count || 0}
|
||||
{t('result', 'Result')}
|
||||
</div>
|
||||
{list?.list?.map((item, index) => (
|
||||
<Card key={String(index)} data={item} />
|
||||
))}
|
||||
{/*<Pagination results={list?.count || 0} />*/}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { Orders } from '@prisma/client';
|
||||
export interface Root2 {
|
||||
id: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
buyer: SellerBuyer;
|
||||
seller: SellerBuyer;
|
||||
messages: Message[];
|
||||
orders: Orders[];
|
||||
}
|
||||
export interface SellerBuyer {
|
||||
id: string;
|
||||
name: any;
|
||||
picture: Picture;
|
||||
}
|
||||
export interface Picture {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
export interface Message {
|
||||
id: string;
|
||||
from: string;
|
||||
content: string;
|
||||
groupId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: any;
|
||||
}
|
||||
export const MarketplaceProvider = createContext<{
|
||||
message?: Root2;
|
||||
}>({});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export const Marketplace = () => {
|
||||
return <div />;
|
||||
};
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const OrderList: FC<{
|
||||
type: 'seller' | 'buyer';
|
||||
}> = (props) => {
|
||||
const fetch = useFetch();
|
||||
const { type } = props;
|
||||
const getOrderDetails = useCallback(async () => {
|
||||
return (await fetch(`/marketplace/orders?type=${type}`)).json();
|
||||
}, [type]);
|
||||
const { data, isLoading } = useSWR(
|
||||
`/marketplace/orders/${type}`,
|
||||
getOrderDetails
|
||||
);
|
||||
const t = useT();
|
||||
|
||||
const biggerRow = useMemo(() => {
|
||||
return data?.orders?.reduce((all: any, current: any) => {
|
||||
if (current.details.length > all) return current.details.length;
|
||||
return all;
|
||||
}, 0);
|
||||
}, [data]);
|
||||
if (isLoading || !data?.orders?.length) return <></>;
|
||||
return (
|
||||
<div className="bg-sixth p-[24px] flex flex-col gap-[24px] border border-customColor6 rounded-[4px]">
|
||||
<h3 className="text-[24px]">{t('orders', 'Orders')}</h3>
|
||||
<div className="pt-[20px] px-[24px] border border-customColor6 flex">
|
||||
<table className="w-full">
|
||||
<tr>
|
||||
<td colSpan={biggerRow + 1} className="pb-[20px]">
|
||||
{type === 'seller' ? 'Buyer' : 'Seller'}
|
||||
</td>
|
||||
<td className="pb-[20px]">{t('price', 'Price')}</td>
|
||||
<td className="pb-[20px]">{t('state', 'State')}</td>
|
||||
</tr>
|
||||
{data.orders.map((order: any) => (
|
||||
<tr key={order.id}>
|
||||
<td className="pb-[20px]">{order.name}</td>
|
||||
{order.details.map((details: any, index: number) => (
|
||||
<td
|
||||
className="pb-[20px]"
|
||||
key={details.id}
|
||||
{...(index === order.details.length - 1
|
||||
? {
|
||||
colSpan: biggerRow - order.details.length + 1,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div className="flex gap-[20px] items-center">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={details.integration.picture}
|
||||
alt="platform"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="absolute start-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
|
||||
src={`/icons/platforms/${details.integration.providerIdentifier}.png`}
|
||||
alt={details.integration.name}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{details.integration.name} ({details.total}/
|
||||
{details.submitted})
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
<td className="pb-[20px]">{order.price}</td>
|
||||
<td className="pb-[20px]">{order.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
import React, { FC, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { CustomSelect } from '@gitroom/react/form/custom.select';
|
||||
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Total } from '@gitroom/react/form/total';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { array, number, object, string } from 'yup';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const schema = object({
|
||||
socialMedia: array()
|
||||
.min(1)
|
||||
.of(
|
||||
object({
|
||||
total: number().required(),
|
||||
value: object({
|
||||
value: string().required('Platform is required'),
|
||||
}).required(),
|
||||
price: string().matches(/^\d+$/, 'Price must be a number').required(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
}).required();
|
||||
export const NewOrder: FC<{
|
||||
group: string;
|
||||
}> = (props) => {
|
||||
const { group } = props;
|
||||
const t = useT();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const [update, setUpdate] = useState(0);
|
||||
const toast = useToaster();
|
||||
const loadIntegrations = useCallback(async () => {
|
||||
return (
|
||||
await (await fetch('/integrations/list')).json()
|
||||
).integrations.filter((f: any) => !f.disabled);
|
||||
}, []);
|
||||
const { data } = useSWR('integrations', loadIntegrations);
|
||||
const options: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
}> = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return data?.map((p: any) => ({
|
||||
label: p.name,
|
||||
value: p.identifier,
|
||||
id: p.id,
|
||||
icon: (
|
||||
<div className="relative">
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full"
|
||||
src={p.picture}
|
||||
alt={p.name}
|
||||
/>
|
||||
<img
|
||||
className="absolute start-[10px] top-[10px] w-[15px] h-[15px] rounded-full"
|
||||
src={`/icons/platforms/${p.identifier}.png`}
|
||||
alt={p.name}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
}, [data]);
|
||||
const change = useCallback(() => {
|
||||
setUpdate((prev) => prev + 1);
|
||||
}, [update]);
|
||||
const form = useForm<{
|
||||
price: string;
|
||||
socialMedia: Array<{
|
||||
value?: string;
|
||||
total: number;
|
||||
price: any;
|
||||
}>;
|
||||
}>({
|
||||
values: {
|
||||
price: '',
|
||||
socialMedia: [
|
||||
{
|
||||
value: undefined,
|
||||
total: 1,
|
||||
price: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
criteriaMode: 'all',
|
||||
// @ts-ignore
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'socialMedia',
|
||||
});
|
||||
const possibleOptions = useMemo(() => {
|
||||
return fields.map((z, index) => {
|
||||
const field = form.getValues(`socialMedia.${index}.value`) as {
|
||||
value?: {
|
||||
value?: string;
|
||||
total?: number;
|
||||
};
|
||||
};
|
||||
return options.filter((f) => {
|
||||
const getAllValues = fields.reduce((all, p, innerIndex) => {
|
||||
if (index === innerIndex) {
|
||||
return all;
|
||||
}
|
||||
const newField = form.getValues(
|
||||
`socialMedia.${innerIndex}.value`
|
||||
) as {
|
||||
value?: {
|
||||
value?: string;
|
||||
};
|
||||
};
|
||||
all.push(newField);
|
||||
return all;
|
||||
}, [] as any[]);
|
||||
return (
|
||||
field?.value?.value === f.value ||
|
||||
!getAllValues.some((v) => v?.value === f.value)
|
||||
);
|
||||
});
|
||||
});
|
||||
}, [update, fields, options]);
|
||||
const canAddMoreOptions = useMemo(() => {
|
||||
return fields.length < options.length;
|
||||
}, [update, fields, options]);
|
||||
const close = useCallback(() => {
|
||||
return modal.closeAll();
|
||||
}, []);
|
||||
const submit = useCallback(async (data: any) => {
|
||||
await (
|
||||
await fetch('/marketplace/offer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
group,
|
||||
socialMedia: data.socialMedia.map((z: any) => ({
|
||||
total: z.total,
|
||||
price: +z.price,
|
||||
value: z.value.id,
|
||||
})),
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
toast.show('Offer sent successfully');
|
||||
modal.closeAll();
|
||||
}, []);
|
||||
const totalPrice = useMemo(() => {
|
||||
return fields.reduce((total, field, index) => {
|
||||
return (
|
||||
total +
|
||||
(+(form.getValues(`socialMedia.${index}.price`) || 0) *
|
||||
form.getValues(`socialMedia.${index}.total`) || 0)
|
||||
);
|
||||
}, 0);
|
||||
}, [update, fields, options]);
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="w-full max-w-[647px] mx-auto bg-sixth px-[16px] rounded-[4px] border border-customColor6 gap-[24px] flex flex-col relative">
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute end-[20px] top-[20px] 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="text-[18px] font-[500] flex flex-col">
|
||||
<TopTitle title={`Send a new offer`} />
|
||||
<div className="p-[16px] -mx-[16px]">
|
||||
{fields.map((field, index) => (
|
||||
<div className="relative flex gap-[10px]" key={field.id}>
|
||||
{index !== 0 && (
|
||||
<div
|
||||
onClick={() => remove(index)}
|
||||
className="cursor-pointer top-[3px] z-[99] w-[15px] h-[15px] bg-red-500 rounded-full text-textColor absolute start-[60px] text-[12px] flex justify-center items-center pb-[2px] select-none"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<CustomSelect
|
||||
{...form.register(`socialMedia.${index}.value`)}
|
||||
onChange={change}
|
||||
options={possibleOptions[index]}
|
||||
placeholder="Select social media"
|
||||
label="Platform"
|
||||
translationKey="label_platform"
|
||||
disableForm={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Total
|
||||
customOnChange={change}
|
||||
{...form.register(`socialMedia.${index}.total`)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
icon={<div className="text-[14px]">$</div>}
|
||||
className="text-[14px]"
|
||||
label="Price per post"
|
||||
translationKey="label_price_per_post"
|
||||
error={
|
||||
form.formState.errors?.socialMedia?.[index]?.price
|
||||
?.message
|
||||
}
|
||||
customUpdate={change}
|
||||
name={`socialMedia.${index}.price`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{canAddMoreOptions && (
|
||||
<div>
|
||||
<div
|
||||
onClick={() =>
|
||||
append({
|
||||
value: undefined,
|
||||
total: 1,
|
||||
price: '',
|
||||
})
|
||||
}
|
||||
className="select-none rounded-[4px] border-2 border-customColor21 flex py-[9.5px] px-[24px] items-center gap-[4px] text-[14px] float-left cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M15.75 9C15.75 9.14918 15.6907 9.29226 15.5852 9.39775C15.4798 9.50324 15.3367 9.5625 15.1875 9.5625H9.5625V15.1875C9.5625 15.3367 9.50324 15.4798 9.39775 15.5852C9.29226 15.6907 9.14918 15.75 9 15.75C8.85082 15.75 8.70774 15.6907 8.60225 15.5852C8.49676 15.4798 8.4375 15.3367 8.4375 15.1875V9.5625H2.8125C2.66332 9.5625 2.52024 9.50324 2.41475 9.39775C2.30926 9.29226 2.25 9.14918 2.25 9C2.25 8.85082 2.30926 8.70774 2.41475 8.60225C2.52024 8.49676 2.66332 8.4375 2.8125 8.4375H8.4375V2.8125C8.4375 2.66332 8.49676 2.52024 8.60225 2.41475C8.70774 2.30926 8.85082 2.25 9 2.25C9.14918 2.25 9.29226 2.30926 9.39775 2.41475C9.50324 2.52024 9.5625 2.66332 9.5625 2.8125V8.4375H15.1875C15.3367 8.4375 15.4798 8.49676 15.5852 8.60225C15.6907 8.70774 15.75 8.85082 15.75 9Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
{t('add_another_platform', 'Add another platform')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-[16px] flex justify-end">
|
||||
<Button type="submit" className="rounded-[4px]">
|
||||
{t('send_an_offer_for', 'Send an offer for $')}
|
||||
{totalPrice}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export const OrderInProgress: FC<{
|
||||
group: string;
|
||||
buyer: boolean;
|
||||
order: string;
|
||||
}> = (props) => {
|
||||
const { group, buyer, order } = props;
|
||||
const t = useT();
|
||||
const fetch = useFetch();
|
||||
const completeOrder = useCallback(async () => {
|
||||
if (
|
||||
await deleteDialog(
|
||||
'Are you sure you want to pay the seller and end the order? this is irreversible action'
|
||||
)
|
||||
) {
|
||||
await (
|
||||
await fetch(`/marketplace/offer/${order}/complete`, {
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
}
|
||||
}, [order]);
|
||||
return (
|
||||
<div className="flex gap-[10px]">
|
||||
{buyer && (
|
||||
<div
|
||||
onClick={completeOrder}
|
||||
className="rounded-[34px] border-[1px] border-customColor21 !bg-sixth h-[28px] justify-center items-center text-[12px] px-[12px] flex font-[600] cursor-pointer"
|
||||
>
|
||||
{t('complete_order_and_pay_early', 'Complete order and pay early')}
|
||||
</div>
|
||||
)}
|
||||
<div className="h-[28px] justify-center items-center bg-customColor42 text-[12px] px-[12px] flex rounded-[34px] font-[600]">
|
||||
{t('order_in_progress', 'Order in progress')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const CreateNewOrder: FC<{
|
||||
group: string;
|
||||
}> = (props) => {
|
||||
const { group } = props;
|
||||
const modals = useModals();
|
||||
const t = useT();
|
||||
const createOrder = useCallback(() => {
|
||||
modals.openModal({
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
withCloseButton: false,
|
||||
size: '100%',
|
||||
children: <NewOrder group={group} />,
|
||||
});
|
||||
}, [group]);
|
||||
return (
|
||||
<div
|
||||
className="h-[28px] justify-center items-center bg-customColor42 text-[12px] px-[12px] flex rounded-[34px] font-[600] cursor-pointer"
|
||||
onClick={createOrder}
|
||||
>
|
||||
{t('create_a_new_offer', 'Create a new offer')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
enum OrderOptions {
|
||||
CREATE_A_NEW_ORDER = 'CREATE_A_NEW_ORDER',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
WAITING_PUBLICATION = 'WAITING_PUBLICATION',
|
||||
}
|
||||
export const OrderTopActions = () => {
|
||||
const { message } = useContext(MarketplaceProvider);
|
||||
const user = useUser();
|
||||
const isBuyer = useMemo(() => {
|
||||
return user?.id === message?.buyerId;
|
||||
}, [user, message]);
|
||||
const myOptions: OrderOptions | undefined = useMemo(() => {
|
||||
if (
|
||||
!isBuyer &&
|
||||
(!message?.orders.length ||
|
||||
message.orders[0].status === 'COMPLETED' ||
|
||||
message.orders[0].status === 'CANCELED')
|
||||
) {
|
||||
return OrderOptions.CREATE_A_NEW_ORDER;
|
||||
}
|
||||
if (message?.orders?.[0]?.status === 'PENDING') {
|
||||
return OrderOptions.IN_PROGRESS;
|
||||
}
|
||||
if (message?.orders?.[0]?.status === 'ACCEPTED') {
|
||||
return OrderOptions.WAITING_PUBLICATION;
|
||||
}
|
||||
}, [isBuyer, user, message]);
|
||||
if (!myOptions) {
|
||||
return null;
|
||||
}
|
||||
switch (myOptions) {
|
||||
case OrderOptions.CREATE_A_NEW_ORDER:
|
||||
return <CreateNewOrder group={message?.id!} />;
|
||||
case OrderOptions.WAITING_PUBLICATION:
|
||||
return (
|
||||
<OrderInProgress
|
||||
group={message?.id!}
|
||||
buyer={isBuyer}
|
||||
order={message?.orders[0]?.id!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <div />;
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import 'reflect-metadata';
|
||||
import { FC } from 'react';
|
||||
import { Post as PrismaPost } from '.prisma/client';
|
||||
import { Providers } from '@gitroom/frontend/components/new-launch/providers/show.all.providers';
|
||||
export const PreviewPopupDynamic: FC<{
|
||||
postId: string;
|
||||
providerId: string;
|
||||
post: {
|
||||
integration: string;
|
||||
group: string;
|
||||
posts: PrismaPost[];
|
||||
settings: any;
|
||||
};
|
||||
}> = (props) => {
|
||||
const { component: ProviderComponent } = Providers.find(
|
||||
(p) => p.identifier === props.providerId
|
||||
)!;
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { Slider } from '@gitroom/react/form/slider';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { tagsList } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
|
||||
import { Options } from '@gitroom/frontend/components/marketplace/buyer';
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { OrderList } from '@gitroom/frontend/components/marketplace/order.list';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { countries } from '@gitroom/nestjs-libraries/services/stripe.country.list';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const AddAccount: FC<{
|
||||
openBankAccount: (country: string) => void;
|
||||
}> = (props) => {
|
||||
const { openBankAccount } = props;
|
||||
const t = useT();
|
||||
|
||||
const [country, setCountry] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<div className="bg-sixth p-[32px] text-[20px] w-full max-w-[600px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-customColor6 relative">
|
||||
{t(
|
||||
'please_select_your_country_where_your_business_is',
|
||||
'Please select your country where your business is.'
|
||||
)}
|
||||
<br />
|
||||
<Select
|
||||
label="Country"
|
||||
name="country"
|
||||
disableForm={true}
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
>
|
||||
<option value="">{t('select_country', '--SELECT COUNTRY--')}</option>
|
||||
{countries.map((country) => (
|
||||
<option key={country.value} value={country.value}>
|
||||
{country.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!country}
|
||||
loading={loading}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
openBankAccount(country);
|
||||
setLoading(true);
|
||||
}}
|
||||
>
|
||||
{t('connect_bank_account', 'Connect Bank Account')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Seller = () => {
|
||||
const fetch = useFetch();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [keys, setKeys] = useState<
|
||||
Array<{
|
||||
key: string;
|
||||
id: string;
|
||||
user: string;
|
||||
}>
|
||||
>([]);
|
||||
const [connectedLoading, setConnectedLoading] = useState(false);
|
||||
const [state, setState] = useState(true);
|
||||
const [audience, setAudience] = useState<number>(0);
|
||||
const modals = useModals();
|
||||
const accountInformation = useCallback(async () => {
|
||||
const account = await (
|
||||
await fetch('/marketplace/account', {
|
||||
method: 'GET',
|
||||
})
|
||||
).json();
|
||||
setState(account.marketplace);
|
||||
setAudience(account.audience);
|
||||
return account;
|
||||
}, []);
|
||||
const onChange = useCallback((key: string, state: boolean) => {
|
||||
fetch('/marketplace/item', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
key,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
const connectBankAccountLink = useCallback(async (country: string) => {
|
||||
setConnectedLoading(true);
|
||||
const { url } = await (
|
||||
await fetch(`/marketplace/bank?country=${country}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
).json();
|
||||
window.location.href = url;
|
||||
}, []);
|
||||
const loadItems = useCallback(async () => {
|
||||
const data = await (
|
||||
await fetch('/marketplace/item', {
|
||||
method: 'GET',
|
||||
})
|
||||
).json();
|
||||
setKeys(data);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const changeAudienceBackend = useDebouncedCallback(
|
||||
useCallback(async (aud: number) => {
|
||||
fetch('/marketplace/audience', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
audience: aud,
|
||||
}),
|
||||
});
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
const changeAudience = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const num = String(+e.target.value.replace(/\D/g, '') || 0).slice(0, 8);
|
||||
setAudience(+num);
|
||||
changeAudienceBackend(+num);
|
||||
}, []);
|
||||
const changeMarketplace = useCallback(
|
||||
async (value: string) => {
|
||||
await fetch('/marketplace/active', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
active: value === 'on',
|
||||
}),
|
||||
});
|
||||
setState(!state);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const t = useT();
|
||||
|
||||
const { data } = useSWR('/marketplace/account', accountInformation);
|
||||
const connectBankAccount = useCallback(async () => {
|
||||
if (!data?.connectedAccount) {
|
||||
modals.openModal({
|
||||
size: '100%',
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
withCloseButton: false,
|
||||
children: <AddAccount openBankAccount={connectBankAccountLink} />,
|
||||
});
|
||||
return;
|
||||
}
|
||||
connectBankAccountLink('');
|
||||
}, [data, connectBankAccountLink]);
|
||||
useEffect(() => {
|
||||
loadItems();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<OrderList type="seller" />
|
||||
<div className="flex mt-[29px] w-full gap-[26px]">
|
||||
<div className="w-[328px] flex flex-col gap-[16px]">
|
||||
<h2 className="text-[20px]">{t('seller_mode', 'Seller Mode')}</h2>
|
||||
<div className="flex p-[24px] bg-sixth rounded-[4px] border border-customColor6 flex-col items-center gap-[16px]">
|
||||
<div className="w-[64px] h-[64px] bg-customColor38 rounded-full">
|
||||
{!!data?.picture?.path && (
|
||||
<img
|
||||
className="w-full h-full rounded-full"
|
||||
src={data?.picture?.path || ''}
|
||||
alt="avatar"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[24px]">{data?.fullname || ''}</div>
|
||||
{data?.connectedAccount && (
|
||||
<div className="flex gap-[16px] items-center pb-[8px]">
|
||||
<Slider
|
||||
fill={true}
|
||||
value={state ? 'on' : 'off'}
|
||||
onChange={changeMarketplace}
|
||||
/>
|
||||
<div className="text-[18px]">{t('active', 'Active')}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-t-customColor43 w-full" />
|
||||
<div className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={connectBankAccount}
|
||||
loading={connectedLoading}
|
||||
>
|
||||
{!data?.connectedAccount
|
||||
? 'Connect Bank Account'
|
||||
: 'Update Bank Account'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex gap-[16px] flex-col">
|
||||
<h2 className="text-[20px]">{t('details', 'Details')}</h2>
|
||||
<div className="bg-sixth rounded-[4px] border border-customColor6">
|
||||
{tagsList.map((tag) => (
|
||||
<Options
|
||||
rows={3}
|
||||
key={tag.key}
|
||||
onChange={onChange}
|
||||
preSelected={keys.map((key) => key.key)}
|
||||
search={false}
|
||||
options={tag.options}
|
||||
title={tag.name}
|
||||
/>
|
||||
))}
|
||||
<div className="h-[56px] text-[20px] font-[600] flex items-center px-[24px] bg-customColor8">
|
||||
{t('audience_size', 'Audience Size')}
|
||||
</div>
|
||||
<div className="bg-customColor3 flex px-[32px] py-[24px]">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Audience size on all platforms"
|
||||
name="audience"
|
||||
type="text"
|
||||
pattern="\d*"
|
||||
max={8}
|
||||
disableForm={true}
|
||||
value={audience}
|
||||
onChange={changeAudience}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, useCallback, useContext, useMemo } from 'react';
|
||||
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { capitalize } from 'lodash';
|
||||
import removeMd from 'remove-markdown';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { Post as PrismaPost } from '@prisma/client';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import dayjs from 'dayjs';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
const PreviewPopupDynamic = dynamic(() =>
|
||||
import('@gitroom/frontend/components/marketplace/preview.popup.dynamic').then(
|
||||
(mod) => mod.PreviewPopupDynamic
|
||||
)
|
||||
);
|
||||
interface SpecialMessageInterface {
|
||||
type: string;
|
||||
data: {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
export const OrderCompleted: FC = () => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<div className="border border-customColor44 flex flex-col rounded-[6px] overflow-hidden">
|
||||
<div className="flex items-center bg-customColor8 px-[24px] py-[16px] text-[20px]">
|
||||
<div className="flex-1">{t('order_completed', 'Order completed')}</div>
|
||||
</div>
|
||||
<div className="py-[16px] px-[24px] flex flex-col gap-[20px] text-[18px]">
|
||||
{t('the_order_has_been_completed', 'The order has been completed')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Published: FC<{
|
||||
isCurrentOrder: boolean;
|
||||
isSellerOrBuyer: 'BUYER' | 'SELLER';
|
||||
orderStatus: string;
|
||||
data: SpecialMessageInterface;
|
||||
}> = (props) => {
|
||||
const t = useT();
|
||||
const { data, isSellerOrBuyer } = props;
|
||||
return (
|
||||
<div className="border border-customColor44 flex flex-col rounded-[6px] overflow-hidden">
|
||||
<div className="flex items-center bg-customColor8 px-[24px] py-[16px] text-[20px]">
|
||||
<div className="flex-1">
|
||||
{isSellerOrBuyer === 'BUYER' ? 'Your' : 'The'}
|
||||
{t('post_has_been_published', 'post has been published')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-[16px] px-[24px] flex flex-col gap-[20px]">
|
||||
<div className="flex gap-[20px]">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={data.data.picture}
|
||||
alt="platform"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="absolute start-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
|
||||
src={`/icons/platforms/${data.data.integration}.png`}
|
||||
alt={data.data.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-[18px]">{data.data.name}</div>
|
||||
</div>
|
||||
<div className="text-[14px]">
|
||||
{t('url_1', 'URL:')}
|
||||
<a className="underline hover:font-bold" href={data.data.url}>
|
||||
{data.data.url}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const PreviewPopup: FC<{
|
||||
postId: string;
|
||||
providerId: string;
|
||||
post: {
|
||||
integration: string;
|
||||
group: string;
|
||||
posts: PrismaPost[];
|
||||
settings: any;
|
||||
};
|
||||
}> = (props) => {
|
||||
const modal = useModals();
|
||||
const close = useCallback(() => {
|
||||
return modal.closeAll();
|
||||
}, []);
|
||||
return (
|
||||
<div className="bg-primary p-[20px] w-full relative">
|
||||
<button
|
||||
onClick={close}
|
||||
className="outline-none absolute end-[20px] top-[20px] 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>
|
||||
<PreviewPopupDynamic {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Offer: FC<{
|
||||
isCurrentOrder: boolean;
|
||||
isSellerOrBuyer: 'BUYER' | 'SELLER';
|
||||
orderStatus: string;
|
||||
data: SpecialMessageInterface;
|
||||
}> = (props) => {
|
||||
const { data, isSellerOrBuyer, isCurrentOrder, orderStatus } = props;
|
||||
const fetch = useFetch();
|
||||
const acceptOrder = useCallback(async () => {
|
||||
const { url } = await (
|
||||
await fetch(`/marketplace/orders/${data.data.id}/payment`, {
|
||||
method: 'POST',
|
||||
})
|
||||
).json();
|
||||
window.location.href = url;
|
||||
}, [data.data.id]);
|
||||
const totalPrice = useMemo(() => {
|
||||
return data?.data?.ordersItems?.reduce((all: any, current: any) => {
|
||||
return all + current.price * current.quantity;
|
||||
}, 0);
|
||||
}, [data?.data?.ordersItems]);
|
||||
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<div className="border border-customColor44 flex flex-col rounded-[6px] overflow-hidden">
|
||||
<div className="flex items-center bg-customColor8 px-[24px] py-[16px] text-[20px]">
|
||||
<div className="flex-1">{t('new_offer', 'New Offer')}</div>
|
||||
<div className="text-customColor42">${totalPrice}</div>
|
||||
</div>
|
||||
<div className="py-[16px] px-[24px] flex flex-col gap-[20px]">
|
||||
<div className="text-inputText text-[12px]">
|
||||
{t('platform', 'Platform')}
|
||||
</div>
|
||||
{data.data.ordersItems.map((item: any) => (
|
||||
<div
|
||||
key={item.integration.id}
|
||||
className="flex gap-[10px] items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={item.integration.picture}
|
||||
alt="platform"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="absolute start-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
|
||||
src={`/icons/platforms/${item.integration.providerIdentifier}.png`}
|
||||
alt={item.integration.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 text-[18px]">{item.integration.name}</div>
|
||||
<div className="text-[18px]">
|
||||
{item.quantity}
|
||||
{t('posts', 'Posts')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{orderStatus === 'PENDING' &&
|
||||
isCurrentOrder &&
|
||||
isSellerOrBuyer === 'BUYER' && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
className="rounded-[4px] text-[14px]"
|
||||
onClick={acceptOrder}
|
||||
>
|
||||
{t('pay_accept_offer', 'Pay & Accept Offer')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{orderStatus === 'ACCEPTED' && (
|
||||
<div className="flex justify-end">
|
||||
<Button className="rounded-[4px] text-[14px] border border-tableBorder !bg-sixth text-tableBorder">
|
||||
{t('accepted', 'Accepted')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Post: FC<{
|
||||
isCurrentOrder: boolean;
|
||||
isSellerOrBuyer: 'BUYER' | 'SELLER';
|
||||
orderStatus: string;
|
||||
message: string;
|
||||
data: SpecialMessageInterface;
|
||||
}> = (props) => {
|
||||
const { data, isSellerOrBuyer, message, isCurrentOrder, orderStatus } = props;
|
||||
const fetch = useFetch();
|
||||
const modal = useModals();
|
||||
const getIntegration = useCallback(async () => {
|
||||
return (
|
||||
await fetch(
|
||||
`/integrations/${data.data.integration}?order=${data.data.id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
}, []);
|
||||
const requestRevision = useCallback(async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to request a revision?',
|
||||
'Yes'
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await fetch(`/marketplace/posts/${data.data.postId}/revision`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}, [data]);
|
||||
const requestApproved = useCallback(async () => {
|
||||
if (
|
||||
!(await deleteDialog(
|
||||
'Are you sure you want to approve this post?',
|
||||
'Yes'
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await fetch(`/marketplace/posts/${data.data.postId}/approve`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}, [data]);
|
||||
const preview = useCallback(async () => {
|
||||
const post = await (
|
||||
await fetch(`/marketplace/posts/${data.data.postId}`)
|
||||
).json();
|
||||
const integration = await getIntegration();
|
||||
modal.openModal({
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
size: 'auto',
|
||||
withCloseButton: false,
|
||||
children: (
|
||||
<IntegrationContext.Provider
|
||||
value={{
|
||||
allIntegrations: [],
|
||||
date: newDayjs(),
|
||||
integration,
|
||||
value: [],
|
||||
}}
|
||||
>
|
||||
<PreviewPopup
|
||||
providerId={post?.providerId!}
|
||||
post={post}
|
||||
postId={data?.data?.postId!}
|
||||
/>
|
||||
</IntegrationContext.Provider>
|
||||
),
|
||||
});
|
||||
}, [data?.data]);
|
||||
const { data: integrationData } = useSWR<{
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
providerIdentifier: string;
|
||||
}>(`/integrations/${data.data.integration}`, getIntegration);
|
||||
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<div className="border border-customColor44 flex flex-col rounded-[6px] overflow-hidden">
|
||||
<div className="flex items-center bg-customColor8 px-[24px] py-[16px] text-[20px]">
|
||||
<div className="flex-1">
|
||||
{t('post_draft', 'Post Draft')}
|
||||
{capitalize(integrationData?.providerIdentifier || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-[16px] px-[24px] flex gap-[20px]">
|
||||
<div>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={integrationData?.picture}
|
||||
alt="platform"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
<img
|
||||
className="absolute start-[15px] top-[15px] w-[15px] h-[15px] rounded-full"
|
||||
src={`/icons/platforms/${integrationData?.providerIdentifier}.png`}
|
||||
alt={integrationData?.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col text-[16px] gap-[2px]">
|
||||
<div className="text-[18px]">{integrationData?.name}</div>
|
||||
<div>{removeMd(data.data.description)}</div>
|
||||
{isSellerOrBuyer === 'BUYER' &&
|
||||
isCurrentOrder &&
|
||||
data.data.status === 'PENDING' &&
|
||||
orderStatus === 'ACCEPTED' && (
|
||||
<div className="mt-[18px] flex gap-[10px] justify-end">
|
||||
<Button
|
||||
onClick={requestRevision}
|
||||
className="rounded-[4px] text-[14px] border-[2px] border-customColor21 !bg-sixth"
|
||||
>
|
||||
{t('revision_needed', 'Revision Needed')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={requestApproved}
|
||||
className="rounded-[4px] text-[14px] border-[2px] border-customColor21 !bg-sixth"
|
||||
>
|
||||
{t('approve', 'Approve')}
|
||||
</Button>
|
||||
<Button className="rounded-[4px]" onClick={preview}>
|
||||
{t('preview', 'Preview')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.data.status === 'REVISION' && (
|
||||
<div className="flex justify-end">
|
||||
<Button className="rounded-[4px] text-[14px] border border-tableBorder !bg-sixth text-tableBorder">
|
||||
{t('revision_requested', 'Revision Requested')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{data.data.status === 'APPROVED' && (
|
||||
<div className="flex justify-end gap-[10px]">
|
||||
<Button className="rounded-[4px] text-[14px] border border-tableBorder !bg-sixth text-tableBorder">
|
||||
{t('accepted_1', 'ACCEPTED')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.data.status === 'CANCELED' && (
|
||||
<div className="flex justify-end gap-[10px]">
|
||||
<Button className="rounded-[4px] text-[14px] border border-tableBorder !bg-sixth text-tableBorder">
|
||||
{t('cancelled_by_the_seller', 'Cancelled by the seller')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const SpecialMessage: FC<{
|
||||
data: SpecialMessageInterface;
|
||||
id: string;
|
||||
}> = (props) => {
|
||||
const { data, id } = props;
|
||||
const { message } = useContext(MarketplaceProvider);
|
||||
const user = useUser();
|
||||
const isCurrentOrder = useMemo(() => {
|
||||
return message?.orders?.[0]?.id === data?.data?.id;
|
||||
}, [message, data]);
|
||||
const isSellerOrBuyer = useMemo(() => {
|
||||
return user?.id === message?.buyerId ? 'BUYER' : 'SELLER';
|
||||
}, [user, message]);
|
||||
if (data.type === 'offer') {
|
||||
return (
|
||||
<Offer
|
||||
data={data}
|
||||
orderStatus={message?.orders?.[0]?.status!}
|
||||
isCurrentOrder={isCurrentOrder}
|
||||
isSellerOrBuyer={isSellerOrBuyer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (data.type === 'post') {
|
||||
return (
|
||||
<Post
|
||||
data={data}
|
||||
orderStatus={message?.orders?.[0]?.status!}
|
||||
isCurrentOrder={isCurrentOrder}
|
||||
isSellerOrBuyer={isSellerOrBuyer}
|
||||
message={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (data.type === 'published') {
|
||||
return (
|
||||
<Published
|
||||
data={data}
|
||||
orderStatus={message?.orders?.[0]?.status!}
|
||||
isCurrentOrder={isCurrentOrder}
|
||||
isSellerOrBuyer={isSellerOrBuyer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (data.type === 'order-completed') {
|
||||
return <OrderCompleted />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { FC, ReactNode, useCallback, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useSWR from 'swr';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import {
|
||||
MarketplaceProvider,
|
||||
Root2,
|
||||
} from '@gitroom/frontend/components/marketplace/marketplace.provider';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
const Card: FC<{
|
||||
message: Root2;
|
||||
}> = (props) => {
|
||||
const { message } = props;
|
||||
const path = useParams();
|
||||
const router = useRouter();
|
||||
const user = useUser();
|
||||
const changeConversation = useCallback(() => {
|
||||
router.push(`/messages/${message.id}`);
|
||||
}, []);
|
||||
const t = useT();
|
||||
|
||||
const showFrom = useMemo(() => {
|
||||
return user?.id === message?.buyerId ? message?.seller : message?.buyer;
|
||||
}, [message, user]);
|
||||
return (
|
||||
<div
|
||||
onClick={changeConversation}
|
||||
className={clsx(
|
||||
'h-[89px] p-[24px] flex gap-[16px] rounded-[4px] cursor-pointer',
|
||||
path?.id === message.id && 'bg-sixth border border-customColor6'
|
||||
)}
|
||||
>
|
||||
<div className="w-[40px] h-[40px] rounded-full bg-amber-200">
|
||||
{showFrom?.picture?.path && (
|
||||
<img
|
||||
src={showFrom.picture.path}
|
||||
alt={showFrom.name || 'Noname'}
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute start-0 top-0 w-full h-full flex flex-col whitespace-nowrap">
|
||||
<div>{showFrom?.name || 'Noname'}</div>
|
||||
<div className="text-[12px] w-full overflow-ellipsis overflow-hidden">
|
||||
{message.messages[0]?.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px]">{t('mar_28', 'Mar 28')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Layout: FC<{
|
||||
renderChildren: ReactNode;
|
||||
}> = (props) => {
|
||||
const { renderChildren } = props;
|
||||
const fetch = useFetch();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const loadMessagesGroup = useCallback(async () => {
|
||||
return await (await fetch('/messages')).json();
|
||||
}, []);
|
||||
const messagesGroup = useSWR<Root2[]>('messagesGroup', loadMessagesGroup, {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const marketplace = useCallback(() => {
|
||||
router.push('/marketplace');
|
||||
}, [router]);
|
||||
const currentMessage = useMemo(() => {
|
||||
return messagesGroup?.data?.find((message) => message.id === params.id);
|
||||
}, [params.id, messagesGroup.data]);
|
||||
if (messagesGroup.isLoading) {
|
||||
return null;
|
||||
}
|
||||
if (!messagesGroup.isLoading && !messagesGroup?.data?.length) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center mt-[100px] gap-[27px] text-center">
|
||||
<div>
|
||||
<img src="/peoplemarketplace.svg" />
|
||||
</div>
|
||||
<div className="text-[48px]">
|
||||
{t('there_are_no_messages_yet', 'There are no messages yet.')}
|
||||
<br />
|
||||
{t('checkout_the_marketplace', 'Checkout the Marketplace')}
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={marketplace}>
|
||||
{t('go_to_marketplace', 'Go to marketplace')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-[20px]">
|
||||
<div className="pt-[7px] w-[330px] flex flex-col">
|
||||
<div className="text-[20px] mb-[18px]">
|
||||
{t('all_messages', 'All Messages')}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{messagesGroup.data?.map((message) => (
|
||||
<Card key={message.id} message={message} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<MarketplaceProvider.Provider
|
||||
value={{
|
||||
message: currentMessage,
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 flex flex-col">{renderChildren}</div>
|
||||
</MarketplaceProvider.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
'use client';
|
||||
|
||||
export interface Root {
|
||||
id: string;
|
||||
buyerId: string;
|
||||
sellerId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messages: Message[];
|
||||
}
|
||||
export interface SellerBuyer {
|
||||
id: string;
|
||||
name?: string;
|
||||
picture: Picture;
|
||||
}
|
||||
export interface Picture {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
export interface Message {
|
||||
id: string;
|
||||
from: string;
|
||||
content: string;
|
||||
special?: string;
|
||||
groupId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: any;
|
||||
}
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import clsx from 'clsx';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
FC,
|
||||
UIEventHandler,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { reverse } from 'lodash';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { OrderTopActions } from '@gitroom/frontend/components/marketplace/order.top.actions';
|
||||
import { MarketplaceProvider } from '@gitroom/frontend/components/marketplace/marketplace.provider';
|
||||
import { SpecialMessage } from '@gitroom/frontend/components/marketplace/special.message';
|
||||
import { usePageVisibility } from '@gitroom/react/helpers/use.is.visible';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
export const Message: FC<{
|
||||
message: Message;
|
||||
seller: SellerBuyer;
|
||||
buyer: SellerBuyer;
|
||||
scrollDown: () => void;
|
||||
}> = (props) => {
|
||||
const { message, seller, buyer, scrollDown } = props;
|
||||
const user = useUser();
|
||||
const t = useT();
|
||||
const amITheBuyerOrSeller = useMemo(() => {
|
||||
return user?.id === buyer?.id ? 'BUYER' : 'SELLER';
|
||||
}, [buyer, user]);
|
||||
useEffect(() => {
|
||||
scrollDown();
|
||||
}, []);
|
||||
const person = useMemo(() => {
|
||||
if (message.from === 'BUYER') {
|
||||
return buyer;
|
||||
}
|
||||
if (message.from === 'SELLER') {
|
||||
return seller;
|
||||
}
|
||||
}, [amITheBuyerOrSeller, buyer, seller, message]);
|
||||
const data = useMemo(() => {
|
||||
if (!message.special) {
|
||||
return false;
|
||||
}
|
||||
return JSON.parse(message.special);
|
||||
}, [message]);
|
||||
const isMe = useMemo(() => {
|
||||
return (
|
||||
(amITheBuyerOrSeller === 'BUYER' && message.from === 'BUYER') ||
|
||||
(amITheBuyerOrSeller === 'SELLER' && message.from === 'SELLER')
|
||||
);
|
||||
}, [amITheBuyerOrSeller, message]);
|
||||
const time = useMemo(() => {
|
||||
return newDayjs(message.createdAt).format('h:mm A');
|
||||
}, [message]);
|
||||
return (
|
||||
<div className="flex gap-[10px]">
|
||||
<div>
|
||||
<div className="w-[24px] h-[24px] rounded-full bg-amber-200">
|
||||
{!!person?.picture?.path && (
|
||||
<img
|
||||
src={person.picture.path}
|
||||
alt="person"
|
||||
className="w-[24px] h-[24px] rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col max-w-[534px] gap-[10px]">
|
||||
<div className="flex gap-[10px] items-center">
|
||||
<div>{isMe ? t('me', 'Me') : person?.name}</div>
|
||||
<div className="w-[6px] h-[6px] bg-customColor34 rounded-full" />
|
||||
<div className="text-[14px] text-inputText">{time}</div>
|
||||
</div>
|
||||
<pre
|
||||
className={clsx(
|
||||
'whitespace-pre-line font-[400] text-[12px]',
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
{data && <SpecialMessage data={data} id={message.id} />}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Page: FC<{
|
||||
page: number;
|
||||
group: string;
|
||||
refChange: any;
|
||||
}> = (props) => {
|
||||
const { page, group, refChange } = props;
|
||||
const fetch = useFetch();
|
||||
const { message } = useContext(MarketplaceProvider);
|
||||
const visible = usePageVisibility(page);
|
||||
const loadMessages = useCallback(async () => {
|
||||
return await (await fetch(`/messages/${group}/${page}`)).json();
|
||||
}, []);
|
||||
const { data, mutate } = useSWR<Root>(`load-${page}-${group}`, loadMessages, {
|
||||
...(page === 1
|
||||
? {
|
||||
refreshInterval: visible ? 5000 : 0,
|
||||
refreshWhenHidden: false,
|
||||
refreshWhenOffline: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const scrollDown = useCallback(() => {
|
||||
if (page > 1) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
refChange.current?.scrollTo(0, refChange.current.scrollHeight);
|
||||
}, [refChange]);
|
||||
const messages = useMemo(() => {
|
||||
return reverse([...(data?.messages || [])]);
|
||||
}, [data]);
|
||||
return (
|
||||
<>
|
||||
{messages.map((m) => (
|
||||
<Message
|
||||
key={m.id}
|
||||
message={m}
|
||||
seller={message?.seller!}
|
||||
buyer={message?.buyer!}
|
||||
scrollDown={scrollDown}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const Messages = () => {
|
||||
const [pages, setPages] = useState([makeId(3)]);
|
||||
const user = useUser();
|
||||
const params = useParams();
|
||||
const fetch = useFetch();
|
||||
const t = useT();
|
||||
|
||||
const ref = useRef(null);
|
||||
const { message } = useContext(MarketplaceProvider);
|
||||
const showFrom = useMemo(() => {
|
||||
return user?.id === message?.buyerId ? message?.seller : message?.buyer;
|
||||
}, [message, user]);
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(AddMessageDto);
|
||||
}, []);
|
||||
const form = useForm({
|
||||
resolver,
|
||||
values: {
|
||||
message: '',
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
setPages([makeId(3)]);
|
||||
}, [params.id]);
|
||||
const loadMessages = useCallback(async () => {
|
||||
return await (await fetch(`/messages/${params.id}/1`)).json();
|
||||
}, []);
|
||||
const { data, mutate, isLoading } = useSWR<Root>(
|
||||
`load-1-${params.id}`,
|
||||
loadMessages
|
||||
);
|
||||
const submit: SubmitHandler<AddMessageDto> = useCallback(async (values) => {
|
||||
await fetch(`/messages/${params.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
mutate();
|
||||
form.reset();
|
||||
}, []);
|
||||
const changeScroll: UIEventHandler<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
// @ts-ignore
|
||||
if (e.target.scrollTop === 0) {
|
||||
// @ts-ignore
|
||||
e.target.scrollTop = 1;
|
||||
setPages((prev) => [...prev, makeId(3)]);
|
||||
}
|
||||
},
|
||||
[pages, setPages]
|
||||
);
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex-1 flex flex-col rounded-[4px] border border-customColor6 bg-customColor3 pb-[16px]">
|
||||
<div className="bg-customColor8 h-[64px] px-[24px] py-[16px] flex gap-[10px] items-center">
|
||||
<div className="w-[32px] h-[32px] rounded-full bg-amber-200">
|
||||
{!!showFrom?.picture?.path && (
|
||||
<img
|
||||
src={showFrom?.picture?.path}
|
||||
alt="seller"
|
||||
className="w-[32px] h-[32px] rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[20px] flex-1">
|
||||
{showFrom?.name || t('noname', 'Noname')}
|
||||
</div>
|
||||
<div>
|
||||
<OrderTopActions />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-[658px] max-h-[658px] relative">
|
||||
<div
|
||||
className="pt-[18px] pb-[18px] absolute top-0 start-0 w-full h-full px-[24px] flex flex-col gap-[24px] overflow-x-hidden overflow-y-auto"
|
||||
onScroll={changeScroll}
|
||||
ref={ref}
|
||||
>
|
||||
{pages.map((p, index) => (
|
||||
<Page
|
||||
key={'page_' + (pages.length - index)}
|
||||
refChange={ref}
|
||||
page={pages.length - index}
|
||||
group={params.id as string}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-t-customColor46 p-[16px] flex flex-col">
|
||||
<div>
|
||||
<Textarea
|
||||
className="!min-h-[100px] resize-none"
|
||||
label=""
|
||||
name="message"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className={clsx(
|
||||
'rounded-[4px] border border-customColor21 h-[48px] px-[24px]',
|
||||
!form.formState.isValid && 'opacity-40'
|
||||
)}
|
||||
disabled={!form.formState.isValid}
|
||||
>
|
||||
{t('send_message', 'Send Message')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"collection": "@nestjs/schematics",
|
||||
"monorepo": false,
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "../../dist/cron/apps/cron/src/main",
|
||||
"entryFile": "../../dist/orchestrator/apps/orchestrator/src/main",
|
||||
"language": "ts",
|
||||
"generateOptions": {
|
||||
"spec": false
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "postiz-cron",
|
||||
"name": "postiz-orchestrator",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/cron/src/main",
|
||||
"dev": "dotenv -e ../../.env -- nest start --watch --entryFile=./apps/orchestrator/src/main",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/cron/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name cron -- start"
|
||||
"start": "dotenv -e ../../.env -- node --experimental-require-module ./dist/apps/orchestrator/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name orchestrator -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import {
|
||||
NotificationService,
|
||||
NotificationType,
|
||||
} from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { Integration, Post, State } from '@prisma/client';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
|
||||
@Injectable()
|
||||
@Activity()
|
||||
export class AutopostActivity {
|
||||
constructor(private _autoPostService: AutopostService) {}
|
||||
|
||||
@ActivityMethod()
|
||||
async autoPost(id: string) {
|
||||
return this._autoPostService.startAutopost(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Activity, ActivityMethod } from 'nestjs-temporal-core';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
|
||||
@Injectable()
|
||||
@Activity()
|
||||
export class EmailActivity {
|
||||
constructor(
|
||||
private _emailService: EmailService,
|
||||
private _organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
@ActivityMethod()
|
||||
async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
|
||||
return this._emailService.sendEmail(to, subject, html, replyTo);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async getUserOrgs(id: string) {
|
||||
return this._organizationService.getTeam(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Activity,
|
||||
ActivityMethod,
|
||||
TemporalService,
|
||||
} from 'nestjs-temporal-core';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import {
|
||||
NotificationService,
|
||||
NotificationType,
|
||||
} from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { Integration, Post, State } from '@prisma/client';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import {
|
||||
organizationId,
|
||||
postId as postIdSearchParam,
|
||||
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
import { postWorkflow } from '@gitroom/orchestrator/workflows';
|
||||
|
||||
@Injectable()
|
||||
@Activity()
|
||||
export class PostActivity {
|
||||
constructor(
|
||||
private _postService: PostsService,
|
||||
private _notificationService: NotificationService,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _integrationService: IntegrationService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService,
|
||||
private _webhookService: WebhooksService,
|
||||
private _temporalService: TemporalService
|
||||
) {}
|
||||
|
||||
@ActivityMethod()
|
||||
async searchForMissingThreeHoursPosts() {
|
||||
const list = await this._postService.searchForMissingThreeHoursPosts();
|
||||
for (const post of list) {
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
.workflow.signalWithStart('postWorkflow', {
|
||||
workflowId: `post_${post.id}`,
|
||||
taskQueue: 'main',
|
||||
signal: 'poke',
|
||||
signalArgs: [],
|
||||
args: [
|
||||
{
|
||||
taskQueue: post.integration.providerIdentifier
|
||||
.split('-')[0]
|
||||
.toLowerCase(),
|
||||
postId: post.id,
|
||||
organizationId: post.organizationId,
|
||||
},
|
||||
],
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: post.id,
|
||||
},
|
||||
{
|
||||
key: organizationId,
|
||||
value: post.organizationId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async updatePost(id: string, postId: string, releaseURL: string) {
|
||||
return this._postService.updatePost(id, postId, releaseURL);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async getPostsList(orgId: string, postId: string) {
|
||||
return this._postService.getPostsRecursively(postId, true, orgId);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async isCommentable(integration: Integration) {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
return !!getIntegration.comment;
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async postComment(
|
||||
postId: string,
|
||||
lastPostId: string | undefined,
|
||||
integration: Integration,
|
||||
posts: Post[]
|
||||
) {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
const newPosts = await this._postService.updateTags(
|
||||
integration.organizationId,
|
||||
posts
|
||||
);
|
||||
|
||||
return getIntegration.comment(
|
||||
integration.internalId,
|
||||
postId,
|
||||
lastPostId,
|
||||
integration.token,
|
||||
await Promise.all(
|
||||
(newPosts || []).map(async (p) => ({
|
||||
id: p.id,
|
||||
message: stripHtmlValidation(
|
||||
getIntegration.editor,
|
||||
p.content,
|
||||
true,
|
||||
false,
|
||||
!/<\/?[a-z][\s\S]*>/i.test(p.content),
|
||||
getIntegration.mentionFormat
|
||||
),
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
media: await this._postService.updateMedia(
|
||||
p.id,
|
||||
JSON.parse(p.image || '[]'),
|
||||
getIntegration?.convertToJPEG || false
|
||||
),
|
||||
}))
|
||||
),
|
||||
integration
|
||||
);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async postSocial(integration: Integration, posts: Post[]) {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
const newPosts = await this._postService.updateTags(
|
||||
integration.organizationId,
|
||||
posts
|
||||
);
|
||||
|
||||
return getIntegration.post(
|
||||
integration.internalId,
|
||||
integration.token,
|
||||
await Promise.all(
|
||||
(newPosts || []).map(async (p) => ({
|
||||
id: p.id,
|
||||
message: stripHtmlValidation(
|
||||
getIntegration.editor,
|
||||
p.content,
|
||||
true,
|
||||
false,
|
||||
!/<\/?[a-z][\s\S]*>/i.test(p.content),
|
||||
getIntegration.mentionFormat
|
||||
),
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
media: await this._postService.updateMedia(
|
||||
p.id,
|
||||
JSON.parse(p.image || '[]'),
|
||||
getIntegration?.convertToJPEG || false
|
||||
),
|
||||
}))
|
||||
),
|
||||
integration
|
||||
);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async inAppNotification(
|
||||
orgId: string,
|
||||
subject: string,
|
||||
message: string,
|
||||
sendEmail = false,
|
||||
digest = false,
|
||||
type: NotificationType = 'success'
|
||||
) {
|
||||
return this._notificationService.inAppNotification(
|
||||
orgId,
|
||||
subject,
|
||||
message,
|
||||
sendEmail,
|
||||
digest,
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async globalPlugs(integration: Integration) {
|
||||
return this._postService.checkPlugs(
|
||||
integration.organizationId,
|
||||
integration.providerIdentifier,
|
||||
integration.id
|
||||
);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async changeState(id: string, state: State, err?: any, body?: any) {
|
||||
return this._postService.changeState(id, state, err, body);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async internalPlugs(integration: Integration, settings: any) {
|
||||
return this._postService.checkInternalPlug(
|
||||
integration,
|
||||
integration.organizationId,
|
||||
integration.id,
|
||||
settings
|
||||
);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async sendWebhooks(postId: string, orgId: string, integrationId: string) {
|
||||
const webhooks = (await this._webhookService.getWebhooks(orgId)).filter(
|
||||
(f) => {
|
||||
return (
|
||||
f.integrations.length === 0 ||
|
||||
f.integrations.some((i) => i.integration.id === integrationId)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const post = await this._postService.getPostByForWebhookId(postId);
|
||||
return Promise.all(
|
||||
webhooks.map(async (webhook) => {
|
||||
try {
|
||||
await fetch(webhook.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(post),
|
||||
});
|
||||
} catch (e) {
|
||||
/**empty**/
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@ActivityMethod()
|
||||
async processPlug(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
return this._integrationService.processPlugs(data);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async processInternalPlug(data: {
|
||||
post: string;
|
||||
originalIntegration: string;
|
||||
integration: string;
|
||||
plugName: string;
|
||||
orgId: string;
|
||||
delay: number;
|
||||
information: any;
|
||||
}) {
|
||||
return this._integrationService.processInternalPlug(data);
|
||||
}
|
||||
|
||||
@ActivityMethod()
|
||||
async refreshToken(
|
||||
integration: Integration
|
||||
): Promise<false | AuthTokenDetails> {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
try {
|
||||
const refresh = await this._refreshIntegrationService.refresh(
|
||||
integration
|
||||
);
|
||||
if (!refresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getIntegration.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
|
||||
return refresh;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import { getTemporalModule } from '@gitroom/nestjs-libraries/temporal/temporal.module';
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
import { EmailActivity } from '@gitroom/orchestrator/activities/email.activity';
|
||||
|
||||
const activities = [PostActivity, AutopostService, EmailActivity];
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
getTemporalModule(true, require.resolve('./workflows'), activities),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [...activities],
|
||||
get exports() {
|
||||
return [...this.providers, ...this.imports];
|
||||
},
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import 'source-map-support/register';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(utc);
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '@gitroom/orchestrator/app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
// some comment again
|
||||
const app = await NestFactory.createApplicationContext(AppModule);
|
||||
app.enableShutdownHooks();
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { defineSignal } from '@temporalio/workflow';
|
||||
|
||||
export type Email = {
|
||||
message: string;
|
||||
title?: string;
|
||||
type: 'success' | 'fail' | 'info';
|
||||
};
|
||||
|
||||
export const emailSignal = defineSignal<[Email[]]>('email');
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { proxyActivities, sleep } from '@temporalio/workflow';
|
||||
import { AutopostActivity } from '@gitroom/orchestrator/activities/autopost.activity';
|
||||
|
||||
const { autoPost } = proxyActivities<AutopostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue: 'main',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
export async function autoPostWorkflow({
|
||||
id,
|
||||
immediately,
|
||||
}: {
|
||||
id: string;
|
||||
immediately: boolean;
|
||||
}) {
|
||||
while (true) {
|
||||
try {
|
||||
if (immediately) {
|
||||
await autoPost(id);
|
||||
}
|
||||
} catch (err) {}
|
||||
immediately = true;
|
||||
await sleep(3600000);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import {
|
||||
condition,
|
||||
continueAsNew,
|
||||
proxyActivities,
|
||||
setHandler,
|
||||
sleep,
|
||||
} from '@temporalio/workflow';
|
||||
import { Email, emailSignal } from '@gitroom/orchestrator/signals/email.signal';
|
||||
import { EmailActivity } from '@gitroom/orchestrator/activities/email.activity';
|
||||
|
||||
const { sendEmail, getUserOrgs } = proxyActivities<EmailActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue: 'main',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
export async function digestEmailWorkflow({
|
||||
organizationId,
|
||||
queue = [],
|
||||
}: {
|
||||
organizationId: string;
|
||||
queue?: Email[];
|
||||
}) {
|
||||
setHandler(emailSignal, (data) => {
|
||||
queue.push(...data);
|
||||
});
|
||||
|
||||
while (true) {
|
||||
await condition(() => queue.length > 0);
|
||||
await sleep(60000);
|
||||
|
||||
// Take a snapshot batch and immediately clear queue.
|
||||
const batch = queue.splice(0, queue.length);
|
||||
queue = [];
|
||||
|
||||
const org = await getUserOrgs(organizationId);
|
||||
|
||||
for (const user of org.users) {
|
||||
const allowFailure = user.user.sendFailureEmails ? 'fail' : null;
|
||||
const allowSuccess = user.user.sendSuccessEmails ? 'success' : null;
|
||||
|
||||
const toSend = batch.filter(
|
||||
(email) =>
|
||||
email.type === allowFailure ||
|
||||
email.type === allowSuccess ||
|
||||
email.type === 'info'
|
||||
);
|
||||
|
||||
if (toSend.length === 0) continue;
|
||||
|
||||
await sendEmail(
|
||||
user.user.email,
|
||||
toSend.length === 1
|
||||
? toSend[0].title
|
||||
: `[Postiz] Your latest notifications`,
|
||||
toSend.map((p) => p.message).join('<br/>')
|
||||
);
|
||||
}
|
||||
|
||||
return continueAsNew({
|
||||
organizationId,
|
||||
queue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './post.workflow';
|
||||
export * from './autopost.workflow';
|
||||
export * from './digest.email.workflow';
|
||||
export * from './missing.post.workflow';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { proxyActivities, sleep } from '@temporalio/workflow';
|
||||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
|
||||
const { searchForMissingThreeHoursPosts } = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
export async function missingPostWorkflow() {
|
||||
await searchForMissingThreeHoursPosts();
|
||||
while (true) {
|
||||
await sleep('1 hour');
|
||||
await searchForMissingThreeHoursPosts();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
import { PostActivity } from '@gitroom/orchestrator/activities/post.activity';
|
||||
import {
|
||||
ActivityFailure,
|
||||
ApplicationFailure,
|
||||
startChild,
|
||||
proxyActivities,
|
||||
sleep,
|
||||
defineSignal,
|
||||
setHandler,
|
||||
} from '@temporalio/workflow';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { capitalize, sortBy } from 'lodash';
|
||||
import { PostResponse } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { postId as postIdSearchParam } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
const proxyTaskQueue = (taskQueue: string) => {
|
||||
return proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
taskQueue,
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
getPostsList,
|
||||
inAppNotification,
|
||||
changeState,
|
||||
updatePost,
|
||||
sendWebhooks,
|
||||
isCommentable,
|
||||
} = proxyActivities<PostActivity>({
|
||||
startToCloseTimeout: '10 minute',
|
||||
retry: {
|
||||
maximumAttempts: 3,
|
||||
backoffCoefficient: 1,
|
||||
initialInterval: '2 minutes',
|
||||
},
|
||||
});
|
||||
|
||||
const poke = defineSignal('poke');
|
||||
|
||||
export async function postWorkflow({
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow = false,
|
||||
}: {
|
||||
taskQueue: string;
|
||||
postId: string;
|
||||
organizationId: string;
|
||||
postNow?: boolean;
|
||||
}) {
|
||||
// Dynamic task queue, for concurrency
|
||||
const {
|
||||
postSocial,
|
||||
postComment,
|
||||
refreshToken,
|
||||
internalPlugs,
|
||||
globalPlugs,
|
||||
processInternalPlug,
|
||||
processPlug,
|
||||
} = proxyTaskQueue(taskQueue);
|
||||
|
||||
let poked = false;
|
||||
setHandler(poke, () => {
|
||||
poked = true;
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
// get all the posts and comments to post
|
||||
const postsList = await getPostsList(organizationId, postId);
|
||||
const [post] = postsList;
|
||||
|
||||
// in case doesn't exists for some reason, fail it
|
||||
if (!post) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's a repeatable post, we should ignore this
|
||||
if (!postNow) {
|
||||
if (dayjs(post.publishDate).isBefore(dayjs())) {
|
||||
return;
|
||||
}
|
||||
await sleep(dayjs(post.publishDate).diff(dayjs(), 'millisecond'));
|
||||
}
|
||||
|
||||
// if refresh is needed from last time, let's inform the user
|
||||
if (post.integration?.refreshNeeded) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's disabled, inform the user
|
||||
if (post.integration?.disabled) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`We couldn't post to ${post.integration?.providerIdentifier} for ${post?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to post comment for this social?
|
||||
const toComment =
|
||||
postsList.length === 1 ? false : await isCommentable(post.integration);
|
||||
|
||||
// list of all the saved results
|
||||
const postsResults: PostResponse[] = [];
|
||||
|
||||
// iterate over the posts
|
||||
for (let i = 0; i < postsList.length; i++) {
|
||||
// this is a small trick to repeat an action in case of token refresh
|
||||
while (true) {
|
||||
try {
|
||||
// first post the main post
|
||||
if (i === 0) {
|
||||
postsResults.push(
|
||||
...(await postSocial(post.integration as Integration, [
|
||||
postsList[i],
|
||||
]))
|
||||
);
|
||||
|
||||
// then post the comments if any
|
||||
} else {
|
||||
if (!toComment) {
|
||||
break;
|
||||
}
|
||||
postsResults.push(
|
||||
...(await postComment(
|
||||
postsResults[0].postId,
|
||||
postsResults.length === 1
|
||||
? undefined
|
||||
: postsResults[i - 1].postId,
|
||||
post.integration,
|
||||
[postsList[i]]
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// mark post as successful
|
||||
await updatePost(
|
||||
postsList[i].id,
|
||||
postsResults[i].postId,
|
||||
postsResults[i].releaseURL
|
||||
);
|
||||
|
||||
// break the current while to move to the next post
|
||||
break;
|
||||
} catch (err) {
|
||||
// if token refresh is needed, do it and repeat
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
|
||||
// for other errors, change state and inform the user if needed
|
||||
await changeState(postsList[0].id, 'ERROR', err, postsList);
|
||||
|
||||
// specific case for bad body errors
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'bad_body'
|
||||
) {
|
||||
await inAppNotification(
|
||||
post.organizationId,
|
||||
`Error posting on ${post.integration?.providerIdentifier} for ${post?.integration?.name}`,
|
||||
`An error occurred while posting on ${
|
||||
post.integration?.providerIdentifier
|
||||
}${err?.message ? `: ${err?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send notification on a sucessful post
|
||||
await inAppNotification(
|
||||
post.integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
post.integration.providerIdentifier
|
||||
)} at ${postsResults[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
// send webhooks for the post
|
||||
await sendWebhooks(
|
||||
postsResults[0].postId,
|
||||
post.organizationId,
|
||||
post.integration.id
|
||||
);
|
||||
|
||||
// load internal plugs like repost by other users
|
||||
const internalPlugsList = await internalPlugs(
|
||||
post.integration,
|
||||
JSON.parse(post.settings)
|
||||
);
|
||||
|
||||
// load global plugs, like repost a post if it gets to a certain number of likes
|
||||
const globalPlugsList = (await globalPlugs(post.integration)).reduce(
|
||||
(all, current) => {
|
||||
for (let i = 1; i <= current.totalRuns; i++) {
|
||||
all.push({
|
||||
...current,
|
||||
delay: current.delay * i,
|
||||
});
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Check if the post is repeatable
|
||||
const repeatPost = !post.intervalInDays
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'repeat-post',
|
||||
delay:
|
||||
post.intervalInDays * 24 * 60 * 60 * 1000 -
|
||||
(new Date().getTime() - startTime.getTime()),
|
||||
},
|
||||
];
|
||||
|
||||
// Sort all the actions by delay, so we can process them in order
|
||||
const list = sortBy(
|
||||
[...internalPlugsList, ...globalPlugsList, ...repeatPost],
|
||||
'delay'
|
||||
);
|
||||
|
||||
// process all the plugs in order, we are using while because in some cases we need to remove items from the list
|
||||
while (list.length > 0) {
|
||||
// get the next to process
|
||||
const todo = list.shift();
|
||||
|
||||
// wait for the delay
|
||||
await sleep(todo.delay);
|
||||
|
||||
// process internal plug
|
||||
if (todo.type === 'internal-plug') {
|
||||
while (true) {
|
||||
try {
|
||||
await processInternalPlug({ ...todo, post: postsResults[0].postId });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process global plug
|
||||
if (todo.type === 'global') {
|
||||
while (true) {
|
||||
try {
|
||||
const process = await processPlug({
|
||||
...todo,
|
||||
postId: postsResults[0].postId,
|
||||
});
|
||||
if (process) {
|
||||
const toDelete = list
|
||||
.reduce((all, current, index) => {
|
||||
if (current.plugId === todo.plugId) {
|
||||
all.push(index);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, [])
|
||||
.reverse();
|
||||
|
||||
for (const index of toDelete) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof ActivityFailure &&
|
||||
err.cause instanceof ApplicationFailure &&
|
||||
err.cause.type === 'refresh_token'
|
||||
) {
|
||||
const refresh = await refreshToken(post.integration);
|
||||
if (!refresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
post.integration.token = refresh.accessToken;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// process repeat post in a new workflow, this is important so the other plugs can keep running
|
||||
if (todo.type === 'repeat-post') {
|
||||
await startChild(postWorkflow, {
|
||||
parentClosePolicy: 'ABANDON',
|
||||
args: [
|
||||
{
|
||||
taskQueue,
|
||||
postId,
|
||||
organizationId,
|
||||
postNow: true,
|
||||
},
|
||||
],
|
||||
workflowId: `post_${post.id}_${makeId(10)}`,
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: postId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
dist/
|
||||
node_modules/
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"monorepo": false,
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "../../dist/workers/apps/workers/src/main",
|
||||
"language": "ts",
|
||||
"generateOptions": {
|
||||
"spec": false
|
||||
},
|
||||
"compilerOptions": {
|
||||
"manualRestart": true,
|
||||
"tsConfigPath": "./tsconfig.build.json",
|
||||
"webpack": false,
|
||||
"deleteOutDir": true,
|
||||
"assets": [],
|
||||
"watchAssets": false,
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "postiz-workers",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"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 --experimental-require-module ./dist/apps/workers/src/main.js",
|
||||
"pm2": "pm2 start pnpm --name workers -- start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/database.module';
|
||||
import { PostsController } from '@gitroom/workers/app/posts.controller';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { PlugsController } from '@gitroom/workers/app/plugs.controller';
|
||||
import { SentryModule } from '@sentry/nestjs/setup';
|
||||
import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception';
|
||||
|
||||
@Module({
|
||||
imports: [SentryModule.forRoot(), DatabaseModule, BullMqModule],
|
||||
controllers: [PostsController, PlugsController],
|
||||
providers: [FILTER],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
|
||||
@Controller()
|
||||
export class PlugsController {
|
||||
constructor(private _integrationService: IntegrationService) {}
|
||||
|
||||
@EventPattern('plugs', Transport.REDIS)
|
||||
async plug(data: {
|
||||
plugId: string;
|
||||
postId: string;
|
||||
delay: number;
|
||||
totalRuns: number;
|
||||
currentRun: number;
|
||||
}) {
|
||||
try {
|
||||
return await this._integrationService.processPlugs(data);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the plug worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('internal-plugs', Transport.REDIS)
|
||||
async internalPlug(data: {
|
||||
post: string;
|
||||
originalIntegration: string;
|
||||
integration: string;
|
||||
plugName: string;
|
||||
orgId: string;
|
||||
delay: number;
|
||||
information: any;
|
||||
}) {
|
||||
try {
|
||||
return await this._integrationService.processInternalPlug(data);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the internal plugs worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { EventPattern, Transport } from '@nestjs/microservices';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
|
||||
|
||||
@Controller()
|
||||
export class PostsController {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _webhooksService: WebhooksService,
|
||||
private _autopostsService: AutopostService
|
||||
) {}
|
||||
|
||||
@EventPattern('post', Transport.REDIS)
|
||||
async post(data: { id: string }) {
|
||||
console.log('processing', data);
|
||||
try {
|
||||
return await this._postsService.post(data.id);
|
||||
} catch (err) {
|
||||
console.log("Unhandled error, let's avoid crashing the post worker", err);
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('submit', Transport.REDIS)
|
||||
async payout(data: { id: string; releaseURL: string }) {
|
||||
try {
|
||||
return await this._postsService.payout(data.id, data.releaseURL);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the submit worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('sendDigestEmail', Transport.REDIS)
|
||||
async sendDigestEmail(data: { subject: string; org: string; since: string }) {
|
||||
try {
|
||||
return await this._postsService.sendDigestEmail(
|
||||
data.subject,
|
||||
data.org,
|
||||
data.since
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the digest worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('webhooks', Transport.REDIS)
|
||||
async webhooks(data: { org: string; since: string }) {
|
||||
try {
|
||||
return await this._webhooksService.fireWebhooks(data.org, data.since);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the webhooks worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@EventPattern('cron', Transport.REDIS)
|
||||
async cron(data: { id: string }) {
|
||||
try {
|
||||
return await this._autopostsService.startAutopost(data.id);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"Unhandled error, let's avoid crashing the autopost worker",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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';
|
||||
|
||||
async function bootstrap() {
|
||||
process.env.IS_WORKER = 'true';
|
||||
|
||||
// some comment again
|
||||
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
|
||||
AppModule,
|
||||
{
|
||||
strategy: new BullMqServer(),
|
||||
}
|
||||
);
|
||||
|
||||
await app.listen();
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,87 @@ services:
|
|||
- postiz-network
|
||||
restart: always
|
||||
|
||||
temporal-elasticsearch:
|
||||
container_name: temporal-elasticsearch
|
||||
image: elasticsearch:7.17.27
|
||||
environment:
|
||||
- cluster.routing.allocation.disk.threshold_enabled=true
|
||||
- cluster.routing.allocation.disk.watermark.low=512mb
|
||||
- cluster.routing.allocation.disk.watermark.high=256mb
|
||||
- cluster.routing.allocation.disk.watermark.flood_stage=128mb
|
||||
- discovery.type=single-node
|
||||
- ES_JAVA_OPTS=-Xms256m -Xmx256m
|
||||
- xpack.security.enabled=false
|
||||
networks:
|
||||
- temporal-network
|
||||
expose:
|
||||
- 9200
|
||||
volumes:
|
||||
- /var/lib/elasticsearch/data
|
||||
|
||||
temporal-postgresql:
|
||||
container_name: temporal-postgresql
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: temporal
|
||||
POSTGRES_USER: temporal
|
||||
networks:
|
||||
- temporal-network
|
||||
expose:
|
||||
- 5432
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
|
||||
temporal:
|
||||
container_name: temporal
|
||||
ports:
|
||||
- "7233:7233"
|
||||
image: temporalio/auto-setup:1.28.1
|
||||
depends_on:
|
||||
- temporal-postgresql
|
||||
- temporal-elasticsearch
|
||||
environment:
|
||||
- DB=postgres12
|
||||
- DB_PORT=5432
|
||||
- POSTGRES_USER=temporal
|
||||
- POSTGRES_PWD=temporal
|
||||
- POSTGRES_SEEDS=temporal-postgresql
|
||||
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
|
||||
- ENABLE_ES=true
|
||||
- ES_SEEDS=temporal-elasticsearch
|
||||
- ES_VERSION=v7
|
||||
- TEMPORAL_NAMESPACE=default
|
||||
networks:
|
||||
- temporal-network
|
||||
volumes:
|
||||
- ./dynamicconfig:/etc/temporal/config/dynamicconfig
|
||||
labels:
|
||||
kompose.volume.type: configMap
|
||||
|
||||
temporal-admin-tools:
|
||||
container_name: temporal-admin-tools
|
||||
image: temporalio/admin-tools:1.28.1-tctl-1.18.4-cli-1.4.1
|
||||
environment:
|
||||
- TEMPORAL_ADDRESS=temporal:7233
|
||||
- TEMPORAL_CLI_ADDRESS=temporal:7233
|
||||
networks:
|
||||
- temporal-network
|
||||
stdin_open: true
|
||||
depends_on:
|
||||
- temporal
|
||||
tty: true
|
||||
|
||||
temporal-ui:
|
||||
container_name: temporal-ui
|
||||
image: temporalio/ui:2.34.0
|
||||
environment:
|
||||
- TEMPORAL_ADDRESS=temporal:7233
|
||||
- TEMPORAL_CORS_ORIGINS=http://127.0.0.1:3000
|
||||
networks:
|
||||
- temporal-network
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
volumes:
|
||||
redisinsight:
|
||||
postgres-volume:
|
||||
|
|
@ -61,3 +142,6 @@ volumes:
|
|||
networks:
|
||||
postiz-network:
|
||||
external: false
|
||||
temporal-network:
|
||||
driver: bridge
|
||||
name: temporal-network
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
system.forceSearchAttributesCacheRefreshOnRead:
|
||||
- value: true # Dev setup only. Please don't turn this on in production.
|
||||
constraints: {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
limit.maxIDLength:
|
||||
- value: 255
|
||||
constraints: {}
|
||||
system.forceSearchAttributesCacheRefreshOnRead:
|
||||
- value: true # Dev setup only. Please don't turn this on in production.
|
||||
constraints: {}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import Bottleneck from 'bottleneck';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { BadBody } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
|
||||
const connection = new Bottleneck.IORedisConnection({
|
||||
client: ioRedis,
|
||||
});
|
||||
|
||||
const mapper = {} as Record<string, Bottleneck>;
|
||||
|
||||
export const concurrency = async <T>(
|
||||
identifier: string,
|
||||
maxConcurrent = 1,
|
||||
func: (...args: any[]) => Promise<T>,
|
||||
ignoreConcurrency = false
|
||||
) => {
|
||||
const strippedIdentifier = identifier.toLowerCase().split('-')[0];
|
||||
mapper[strippedIdentifier] ??= new Bottleneck({
|
||||
id: strippedIdentifier + '-concurrency-new',
|
||||
maxConcurrent,
|
||||
datastore: 'ioredis',
|
||||
connection,
|
||||
minTime: 1000,
|
||||
});
|
||||
let load: T;
|
||||
|
||||
if (ignoreConcurrency) {
|
||||
return await func();
|
||||
}
|
||||
|
||||
try {
|
||||
load = await mapper[strippedIdentifier].schedule<T>(
|
||||
{ expiration: 60000 },
|
||||
async () => {
|
||||
try {
|
||||
return await func();
|
||||
} catch (err) {}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new BadBody(
|
||||
identifier,
|
||||
JSON.stringify({}),
|
||||
{} as any,
|
||||
`Something is wrong with ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return load;
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [BullMqClient],
|
||||
exports: [BullMqClient],
|
||||
})
|
||||
export class BullMqModule {}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
import { ClientProxy, ReadPacket, WritePacket } from '@nestjs/microservices';
|
||||
import { Queue, QueueEvents } from 'bullmq';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { v4 } from 'uuid';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class BullMqClient extends ClientProxy {
|
||||
queues = new Map<string, Queue>();
|
||||
queueEvents = new Map<string, QueueEvents>();
|
||||
|
||||
async connect(): Promise<any> {
|
||||
return;
|
||||
}
|
||||
|
||||
async close() {
|
||||
return;
|
||||
}
|
||||
|
||||
publish(
|
||||
packet: ReadPacket<any>,
|
||||
callback: (packet: WritePacket<any>) => void
|
||||
) {
|
||||
// console.log('hello');
|
||||
// this.publishAsync(packet, callback);
|
||||
return () => console.log('sent');
|
||||
}
|
||||
|
||||
delete(pattern: string, jobId: string) {
|
||||
const queue = this.getQueue(pattern);
|
||||
return queue.remove(jobId);
|
||||
}
|
||||
|
||||
deleteScheduler(pattern: string, jobId: string) {
|
||||
const queue = this.getQueue(pattern);
|
||||
return queue.removeJobScheduler(jobId);
|
||||
}
|
||||
|
||||
async publishAsync(
|
||||
packet: ReadPacket<any>,
|
||||
callback: (packet: WritePacket<any>) => void
|
||||
) {
|
||||
const queue = this.getQueue(packet.pattern);
|
||||
const queueEvents = this.getQueueEvents(packet.pattern);
|
||||
const job = await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
removeOnComplete: !packet.data.options.attempts,
|
||||
removeOnFail: !packet.data.options.attempts,
|
||||
});
|
||||
|
||||
try {
|
||||
await job.waitUntilFinished(queueEvents);
|
||||
console.log('success');
|
||||
callback({ response: job.returnvalue, isDisposed: true });
|
||||
} catch (err) {
|
||||
console.log('err');
|
||||
callback({ err, isDisposed: true });
|
||||
}
|
||||
}
|
||||
|
||||
getQueueEvents(pattern: string) {
|
||||
return (
|
||||
this.queueEvents.get(pattern) ||
|
||||
new QueueEvents(pattern, {
|
||||
connection: ioRedis,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getQueue(pattern: string) {
|
||||
return (
|
||||
this.queues.get(pattern) ||
|
||||
new Queue(pattern, {
|
||||
connection: ioRedis,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async checkForStuckWaitingJobs(queueName: string) {
|
||||
const queue = this.getQueue(queueName);
|
||||
const getJobs = await queue.getJobs('waiting' as const);
|
||||
const now = Date.now();
|
||||
const thresholdMs = 60 * 60 * 1000;
|
||||
return {
|
||||
valid: !getJobs.some((job) => {
|
||||
const age = now - job.timestamp;
|
||||
return age > thresholdMs;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async dispatchEvent(packet: ReadPacket<any>): Promise<any> {
|
||||
console.log('event to dispatch: ', packet);
|
||||
const queue = this.getQueue(packet.pattern);
|
||||
if (packet?.data?.options?.every) {
|
||||
const { every, immediately } = packet.data.options;
|
||||
const id = packet.data.id ?? v4();
|
||||
await queue.upsertJobScheduler(
|
||||
id,
|
||||
{ every, ...(immediately ? { immediately } : {}) },
|
||||
{
|
||||
name: id,
|
||||
data: packet.data,
|
||||
opts: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await queue.add(packet.pattern, packet.data, {
|
||||
jobId: packet.data.id ?? v4(),
|
||||
...packet.data.options,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import { CustomTransportStrategy, Server } from '@nestjs/microservices';
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
|
||||
export class BullMqServer extends Server implements CustomTransportStrategy {
|
||||
queues: Map<string, Queue>;
|
||||
workers: Worker[] = [];
|
||||
|
||||
/**
|
||||
* This method is triggered when you run "app.listen()".
|
||||
*/
|
||||
listen(callback: () => void) {
|
||||
this.queues = [...this.messageHandlers.keys()].reduce((all, pattern) => {
|
||||
all.set(pattern, new Queue(pattern, { connection: ioRedis }));
|
||||
return all;
|
||||
}, new Map());
|
||||
|
||||
this.workers = Array.from(this.messageHandlers).map(
|
||||
([pattern, handler]) => {
|
||||
return new Worker(
|
||||
pattern,
|
||||
async (job) => {
|
||||
const stream$ = this.transformToObservable(
|
||||
await handler(job.data.payload, job)
|
||||
);
|
||||
|
||||
this.send(stream$, (packet) => {
|
||||
if (packet.err) {
|
||||
return job.discard();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
{
|
||||
maxStalledCount: 10,
|
||||
concurrency: 300,
|
||||
connection: ioRedis,
|
||||
removeOnComplete: {
|
||||
count: 0,
|
||||
},
|
||||
removeOnFail: {
|
||||
count: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is triggered on application shutdown.
|
||||
*/
|
||||
close() {
|
||||
this.workers.map((worker) => worker.close());
|
||||
this.queues.forEach((queue) => queue.close());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
|
||||
import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import dayjs from 'dayjs';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { AutoPost, Integration } from '@prisma/client';
|
||||
|
|
@ -15,6 +14,12 @@ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/po
|
|||
import Parser from 'rss-parser';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { TemporalService } from 'nestjs-temporal-core';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import {
|
||||
organizationId,
|
||||
postId as postIdSearchParam,
|
||||
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
const parser = new Parser();
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
|
|
@ -58,7 +63,7 @@ const dallePrompt = z.object({
|
|||
export class AutopostService {
|
||||
constructor(
|
||||
private _autopostsRepository: AutopostRepository,
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _temporalService: TemporalService,
|
||||
private _integrationService: IntegrationService,
|
||||
private _postsService: PostsService
|
||||
) {}
|
||||
|
|
@ -81,7 +86,7 @@ export class AutopostService {
|
|||
id
|
||||
);
|
||||
|
||||
await this.processCron(body.active, data.id);
|
||||
await this.processCron(body.active, orgId, data.id);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -92,30 +97,39 @@ export class AutopostService {
|
|||
id,
|
||||
active
|
||||
);
|
||||
await this.processCron(active, id);
|
||||
await this.processCron(active, orgId, id);
|
||||
return data;
|
||||
}
|
||||
|
||||
async processCron(active: boolean, id: string) {
|
||||
async processCron(active: boolean, orgId: string, id: string) {
|
||||
if (active) {
|
||||
return this._workerServiceProducer.emit('cron', {
|
||||
id,
|
||||
options: {
|
||||
every: 3600000,
|
||||
immediately: true,
|
||||
},
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
try {
|
||||
return this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
workflowId: `autopost-${id}`,
|
||||
taskQueue: 'main',
|
||||
args: [{ id, immediately: true }],
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: organizationId,
|
||||
value: orgId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return this._workerServiceProducer.deleteScheduler('cron', id);
|
||||
try {
|
||||
return await this._temporalService.terminateWorkflow(`autopost-${id}`);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAutopost(orgId: string, id: string) {
|
||||
const data = await this._autopostsRepository.deleteAutopost(orgId, id);
|
||||
await this.processCron(false, id);
|
||||
await this.processCron(false, orgId, id);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prism
|
|||
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
|
||||
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||
import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
|
||||
import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service';
|
||||
import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository';
|
||||
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
|
||||
import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
|
|
@ -18,10 +16,6 @@ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/me
|
|||
import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository';
|
||||
import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { ItemUserRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.repository';
|
||||
import { ItemUserService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.service';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.repository';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
|
|
@ -55,8 +49,6 @@ import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integration
|
|||
UsersRepository,
|
||||
OrganizationService,
|
||||
OrganizationRepository,
|
||||
StarsService,
|
||||
StarsRepository,
|
||||
SubscriptionService,
|
||||
SubscriptionRepository,
|
||||
NotificationService,
|
||||
|
|
@ -68,18 +60,14 @@ import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integration
|
|||
PostsService,
|
||||
PostsRepository,
|
||||
StripeService,
|
||||
MessagesRepository,
|
||||
SignatureRepository,
|
||||
AutopostRepository,
|
||||
AutopostService,
|
||||
SignatureService,
|
||||
MediaService,
|
||||
MediaRepository,
|
||||
ItemUserRepository,
|
||||
AgenciesService,
|
||||
AgenciesRepository,
|
||||
ItemUserService,
|
||||
MessagesService,
|
||||
IntegrationManager,
|
||||
RefreshIntegrationService,
|
||||
ExtractContentService,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
forwardRef,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import {
|
||||
AnalyticsData,
|
||||
AuthTokenDetails,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { Integration, Organization } from '@prisma/client';
|
||||
|
|
@ -15,11 +20,11 @@ import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abst
|
|||
import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { difference, uniq } from 'lodash';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { TemporalService } from 'nestjs-temporal-core';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
|
|
@ -31,16 +36,18 @@ export class IntegrationService {
|
|||
private _autopostsRepository: AutopostRepository,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
@Inject(forwardRef(() => RefreshIntegrationService))
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
private _refreshIntegrationService: RefreshIntegrationService,
|
||||
private _temporalService: TemporalService
|
||||
) {}
|
||||
|
||||
async changeActiveCron(orgId: string) {
|
||||
const data = await this._autopostsRepository.getAutoposts(orgId);
|
||||
|
||||
for (const item of data.filter((f) => f.active)) {
|
||||
await this._workerServiceProducer.deleteScheduler('cron', item.id);
|
||||
try {
|
||||
await this._temporalService.terminateWorkflow(`autopost-${item.id}`);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -442,40 +449,15 @@ export class IntegrationService {
|
|||
getIntegration.providerIdentifier
|
||||
);
|
||||
|
||||
if (
|
||||
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
|
||||
forceRefresh
|
||||
) {
|
||||
const data = await this._refreshIntegrationService.refresh(
|
||||
getIntegration
|
||||
);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { accessToken } = data;
|
||||
// @ts-ignore
|
||||
await getSocialIntegration?.[getAllInternalPlugs.methodName]?.(
|
||||
getIntegration,
|
||||
originalIntegration,
|
||||
data.post,
|
||||
data.information
|
||||
);
|
||||
|
||||
getIntegration.token = accessToken;
|
||||
|
||||
if (getSocialIntegration.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
await getSocialIntegration?.[getAllInternalPlugs.methodName]?.(
|
||||
getIntegration,
|
||||
originalIntegration,
|
||||
data.post,
|
||||
data.information
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
return this.processInternalPlug(data, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async processPlugs(data: {
|
||||
|
|
@ -487,19 +469,13 @@ export class IntegrationService {
|
|||
}) {
|
||||
const getPlugById = await this._integrationRepository.getPlug(data.plugId);
|
||||
if (!getPlugById) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const integration = this._integrationManager.getSocialIntegration(
|
||||
getPlugById.integration.providerIdentifier
|
||||
);
|
||||
|
||||
const findPlug = this._integrationManager
|
||||
.getAllPlugs()
|
||||
.find(
|
||||
(p) => p.identifier === getPlugById.integration.providerIdentifier
|
||||
)!;
|
||||
|
||||
// @ts-ignore
|
||||
const process = await integration[getPlugById.plugFunction](
|
||||
getPlugById.integration,
|
||||
|
|
@ -511,26 +487,14 @@ export class IntegrationService {
|
|||
);
|
||||
|
||||
if (process) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data.totalRuns === data.currentRun) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + data.postId + '_' + findPlug.identifier,
|
||||
options: {
|
||||
delay: data.delay,
|
||||
},
|
||||
payload: {
|
||||
plugId: data.plugId,
|
||||
postId: data.postId,
|
||||
delay: data.delay,
|
||||
totalRuns: data.totalRuns,
|
||||
currentRun: data.currentRun + 1,
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
async createOrUpdatePlug(
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ItemUserRepository {
|
||||
constructor(private _itemUser: PrismaRepository<'itemUser'>) {}
|
||||
|
||||
addOrRemoveItem(add: boolean, userId: string, item: string) {
|
||||
if (!add) {
|
||||
return this._itemUser.model.itemUser.deleteMany({
|
||||
where: {
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
key: item,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return this._itemUser.model.itemUser.create({
|
||||
data: {
|
||||
key: item,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getItems(userId: string) {
|
||||
return this._itemUser.model.itemUser.findMany({
|
||||
where: {
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ItemUserRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/item.user.repository';
|
||||
|
||||
@Injectable()
|
||||
export class ItemUserService {
|
||||
constructor(private _itemUserRepository: ItemUserRepository) {}
|
||||
|
||||
addOrRemoveItem(add: boolean, userId: string, item: string) {
|
||||
return this._itemUserRepository.addOrRemoveItem(add, userId, item);
|
||||
}
|
||||
|
||||
getItems(userId: string) {
|
||||
return this._itemUserRepository.getItems(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,915 +0,0 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
|
||||
import { From, OrderStatus } from '@prisma/client';
|
||||
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
|
||||
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MessagesRepository {
|
||||
constructor(
|
||||
private _messagesGroup: PrismaRepository<'messagesGroup'>,
|
||||
private _messages: PrismaRepository<'messages'>,
|
||||
private _orders: PrismaRepository<'orders'>,
|
||||
private _organizations: PrismaRepository<'organization'>,
|
||||
private _post: PrismaRepository<'post'>,
|
||||
private _payoutProblems: PrismaRepository<'payoutProblems'>,
|
||||
private _users: PrismaRepository<'user'>
|
||||
) {}
|
||||
|
||||
async createConversation(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
body: NewConversationDto
|
||||
) {
|
||||
const { id } =
|
||||
(await this._messagesGroup.model.messagesGroup.findFirst({
|
||||
where: {
|
||||
buyerOrganizationId: organizationId,
|
||||
buyerId: userId,
|
||||
sellerId: body.to,
|
||||
},
|
||||
})) ||
|
||||
(await this._messagesGroup.model.messagesGroup.create({
|
||||
data: {
|
||||
buyerOrganizationId: organizationId,
|
||||
buyerId: userId,
|
||||
sellerId: body.to,
|
||||
},
|
||||
}));
|
||||
|
||||
await this._messagesGroup.model.messagesGroup.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await this._messages.model.messages.create({
|
||||
data: {
|
||||
groupId: id,
|
||||
from: From.BUYER,
|
||||
content: body.message,
|
||||
},
|
||||
});
|
||||
|
||||
return { id };
|
||||
}
|
||||
|
||||
getOrgByOrder(orderId: string) {
|
||||
return this._orders.model.orders.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
},
|
||||
select: {
|
||||
messageGroup: {
|
||||
select: {
|
||||
buyerOrganizationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMessagesGroup(userId: string, organizationId: string) {
|
||||
return this._messagesGroup.model.messagesGroup.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
buyerOrganizationId: organizationId,
|
||||
buyerId: userId,
|
||||
},
|
||||
{
|
||||
sellerId: userId,
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
seller: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
buyer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orders: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
messages: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
groupId: string,
|
||||
body: AddMessageDto
|
||||
) {
|
||||
const group = await this._messagesGroup.model.messagesGroup.findFirst({
|
||||
where: {
|
||||
id: groupId,
|
||||
OR: [
|
||||
{
|
||||
buyerOrganizationId: orgId,
|
||||
buyerId: userId,
|
||||
},
|
||||
{
|
||||
sellerId: userId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
|
||||
const create = await this.createNewMessage(
|
||||
groupId,
|
||||
group.buyerId === userId ? From.BUYER : From.SELLER,
|
||||
body.message
|
||||
);
|
||||
|
||||
await this._messagesGroup.model.messagesGroup.update({
|
||||
where: {
|
||||
id: groupId,
|
||||
},
|
||||
data: {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (userId === group.buyerId) {
|
||||
return create.group.seller;
|
||||
}
|
||||
|
||||
return create.group.buyer;
|
||||
}
|
||||
|
||||
async updateOrderOnline(userId: string) {
|
||||
await this._users.model.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
lastOnline: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
groupId: string,
|
||||
page: number
|
||||
) {
|
||||
return this._messagesGroup.model.messagesGroup.findFirst({
|
||||
where: {
|
||||
id: groupId,
|
||||
OR: [
|
||||
{
|
||||
buyerOrganizationId: organizationId,
|
||||
buyerId: userId,
|
||||
},
|
||||
{
|
||||
sellerId: userId,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
messages: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 10,
|
||||
skip: (page - 1) * 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createOffer(userId: string, body: CreateOfferDto) {
|
||||
const messageGroup =
|
||||
await this._messagesGroup.model.messagesGroup.findFirst({
|
||||
where: {
|
||||
id: body.group,
|
||||
sellerId: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
buyer: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
orders: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!messageGroup?.id) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
|
||||
if (
|
||||
messageGroup.orders.length &&
|
||||
messageGroup.orders[0].status !== 'COMPLETED' &&
|
||||
messageGroup.orders[0].status !== 'CANCELED'
|
||||
) {
|
||||
throw new Error('Order already exists');
|
||||
}
|
||||
|
||||
const data = await this._orders.model.orders.create({
|
||||
data: {
|
||||
sellerId: userId,
|
||||
buyerId: messageGroup.buyer.id,
|
||||
messageGroupId: messageGroup.id,
|
||||
ordersItems: {
|
||||
createMany: {
|
||||
data: body.socialMedia.map((item) => ({
|
||||
quantity: item.total,
|
||||
integrationId: item.value,
|
||||
price: item.price,
|
||||
})),
|
||||
},
|
||||
},
|
||||
status: 'PENDING',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
ordersItems: {
|
||||
select: {
|
||||
quantity: true,
|
||||
price: true,
|
||||
integration: {
|
||||
select: {
|
||||
name: true,
|
||||
providerIdentifier: true,
|
||||
picture: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await this._messages.model.messages.create({
|
||||
data: {
|
||||
groupId: body.group,
|
||||
from: From.SELLER,
|
||||
content: '',
|
||||
special: JSON.stringify({ type: 'offer', data: data }),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async createNewMessage(
|
||||
group: string,
|
||||
from: From,
|
||||
content: string,
|
||||
special?: object
|
||||
) {
|
||||
return this._messages.model.messages.create({
|
||||
data: {
|
||||
groupId: group,
|
||||
from,
|
||||
content,
|
||||
special: JSON.stringify(special),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
group: {
|
||||
select: {
|
||||
buyer: {
|
||||
select: {
|
||||
lastOnline: true,
|
||||
id: true,
|
||||
organizations: true,
|
||||
},
|
||||
},
|
||||
seller: {
|
||||
select: {
|
||||
lastOnline: true,
|
||||
id: true,
|
||||
organizations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOrderDetails(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
orderId: string
|
||||
) {
|
||||
const order = await this._messagesGroup.model.messagesGroup.findFirst({
|
||||
where: {
|
||||
buyerId: userId,
|
||||
buyerOrganizationId: organizationId,
|
||||
},
|
||||
select: {
|
||||
buyer: true,
|
||||
seller: true,
|
||||
orders: {
|
||||
include: {
|
||||
ordersItems: {
|
||||
select: {
|
||||
quantity: true,
|
||||
integration: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: orderId,
|
||||
status: 'PENDING',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!order?.orders[0]?.id) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
return {
|
||||
buyer: order.buyer,
|
||||
seller: order.seller,
|
||||
order: order.orders[0]!,
|
||||
};
|
||||
}
|
||||
|
||||
async canAddPost(id: string, order: string, integrationId: string) {
|
||||
const findOrder = await this._orders.model.orders.findFirst({
|
||||
where: {
|
||||
id: order,
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
select: {
|
||||
posts: true,
|
||||
ordersItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!findOrder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
findOrder.posts.find(
|
||||
(p) => p.id === id && p.approvedSubmitForOrder === 'YES'
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
findOrder.posts.find(
|
||||
(p) =>
|
||||
p.id === id && p.approvedSubmitForOrder === 'WAITING_CONFIRMATION'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const postsForIntegration = findOrder.ordersItems.filter(
|
||||
(p) => p.integrationId === integrationId
|
||||
);
|
||||
const totalPostsRequired = postsForIntegration.reduce(
|
||||
(acc, item) => acc + item.quantity,
|
||||
0
|
||||
);
|
||||
const usedPosts = findOrder.posts.filter(
|
||||
(p) =>
|
||||
p.integrationId === integrationId &&
|
||||
['WAITING_CONFIRMATION', 'YES'].indexOf(p.approvedSubmitForOrder) > -1
|
||||
).length;
|
||||
|
||||
return totalPostsRequired > usedPosts;
|
||||
}
|
||||
|
||||
changeOrderStatus(
|
||||
orderId: string,
|
||||
status: OrderStatus,
|
||||
paymentIntent?: string
|
||||
) {
|
||||
return this._orders.model.orders.update({
|
||||
where: {
|
||||
id: orderId,
|
||||
},
|
||||
data: {
|
||||
status,
|
||||
captureId: paymentIntent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMarketplaceAvailableOffers(orgId: string, id: string) {
|
||||
const offers = await this._organizations.model.organization.findFirst({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
orderSeller: {
|
||||
where: {
|
||||
status: 'ACCEPTED',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
posts: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
integrationId: true,
|
||||
approvedSubmitForOrder: true,
|
||||
},
|
||||
},
|
||||
messageGroup: {
|
||||
select: {
|
||||
buyerOrganizationId: true,
|
||||
},
|
||||
},
|
||||
buyer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ordersItems: {
|
||||
select: {
|
||||
quantity: true,
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const allOrders =
|
||||
offers?.users.flatMap((user) => user.user.orderSeller) || [];
|
||||
|
||||
const onlyValidItems = allOrders.filter(
|
||||
(order) =>
|
||||
(order.posts.find((p) => p.id === id)
|
||||
? 0
|
||||
: order.posts.filter((f) => f.approvedSubmitForOrder !== 'NO')
|
||||
.length) <
|
||||
order.ordersItems.reduce((acc, item) => acc + item.quantity, 0)
|
||||
);
|
||||
|
||||
return onlyValidItems
|
||||
.map((order) => {
|
||||
const postsNumbers = order.posts
|
||||
.filter(
|
||||
(p) =>
|
||||
['WAITING_CONFIRMATION', 'YES'].indexOf(
|
||||
p.approvedSubmitForOrder
|
||||
) > -1
|
||||
)
|
||||
.reduce((acc, post) => {
|
||||
acc[post.integrationId] = acc[post.integrationId] + 1 || 1;
|
||||
return acc;
|
||||
}, {} as { [key: string]: number });
|
||||
|
||||
const missing = order.ordersItems.map((item) => {
|
||||
return {
|
||||
integration: item,
|
||||
missing: item.quantity - (postsNumbers[item.integration.id] || 0),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
usedIds: order.posts.map((p) => ({
|
||||
id: p.id,
|
||||
status: p.approvedSubmitForOrder,
|
||||
})),
|
||||
buyer: order.buyer,
|
||||
missing,
|
||||
};
|
||||
})
|
||||
.filter((f) => f.missing.length);
|
||||
}
|
||||
|
||||
async requestRevision(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
postId: string,
|
||||
message: string
|
||||
) {
|
||||
const loadMessage = await this._messages.model.messages.findFirst({
|
||||
where: {
|
||||
id: message,
|
||||
group: {
|
||||
buyerOrganizationId: orgId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
special: true,
|
||||
},
|
||||
});
|
||||
|
||||
const post = await this._post.model.post.findFirst({
|
||||
where: {
|
||||
id: postId,
|
||||
approvedSubmitForOrder: 'WAITING_CONFIRMATION',
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (post && loadMessage) {
|
||||
const special = JSON.parse(loadMessage.special!);
|
||||
special.data.status = 'REVISION';
|
||||
await this._messages.model.messages.update({
|
||||
where: {
|
||||
id: message,
|
||||
},
|
||||
data: {
|
||||
special: JSON.stringify(special),
|
||||
},
|
||||
});
|
||||
|
||||
await this._post.model.post.update({
|
||||
where: {
|
||||
id: postId,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: {
|
||||
approvedSubmitForOrder: 'NO',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async requestCancel(orgId: string, postId: string) {
|
||||
const getPost = await this._post.model.post.findFirst({
|
||||
where: {
|
||||
id: postId,
|
||||
organizationId: orgId,
|
||||
approvedSubmitForOrder: {
|
||||
in: ['WAITING_CONFIRMATION', 'YES'],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
lastMessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!getPost) {
|
||||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
await this._post.model.post.update({
|
||||
where: {
|
||||
id: postId,
|
||||
},
|
||||
data: {
|
||||
approvedSubmitForOrder: 'NO',
|
||||
submittedForOrganizationId: null,
|
||||
},
|
||||
});
|
||||
|
||||
const special = JSON.parse(getPost.lastMessage!.special!);
|
||||
special.data.status = 'CANCELED';
|
||||
await this._messages.model.messages.update({
|
||||
where: {
|
||||
id: getPost.lastMessage!.id,
|
||||
},
|
||||
data: {
|
||||
special: JSON.stringify(special),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async requestApproved(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
postId: string,
|
||||
message: string
|
||||
) {
|
||||
const loadMessage = await this._messages.model.messages.findFirst({
|
||||
where: {
|
||||
id: message,
|
||||
group: {
|
||||
buyerOrganizationId: orgId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
special: true,
|
||||
},
|
||||
});
|
||||
|
||||
const post = await this._post.model.post.findFirst({
|
||||
where: {
|
||||
id: postId,
|
||||
approvedSubmitForOrder: 'WAITING_CONFIRMATION',
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (post && loadMessage) {
|
||||
const special = JSON.parse(loadMessage.special!);
|
||||
special.data.status = 'APPROVED';
|
||||
await this._messages.model.messages.update({
|
||||
where: {
|
||||
id: message,
|
||||
},
|
||||
data: {
|
||||
special: JSON.stringify(special),
|
||||
},
|
||||
});
|
||||
|
||||
await this._post.model.post.update({
|
||||
where: {
|
||||
id: postId,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: {
|
||||
approvedSubmitForOrder: 'YES',
|
||||
},
|
||||
});
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
completeOrder(orderId: string) {
|
||||
return this._orders.model.orders.update({
|
||||
where: {
|
||||
id: orderId,
|
||||
},
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async completeOrderAndPay(orgId: string, order: string) {
|
||||
const findOrder = await this._orders.model.orders.findFirst({
|
||||
where: {
|
||||
id: order,
|
||||
messageGroup: {
|
||||
buyerOrganizationId: orgId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
captureId: true,
|
||||
seller: {
|
||||
select: {
|
||||
account: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
ordersItems: true,
|
||||
posts: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!findOrder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const releasedPosts = findOrder.posts.filter((p) => p.releaseURL);
|
||||
const nonReleasedPosts = findOrder.posts.filter((p) => !p.releaseURL);
|
||||
|
||||
const totalPosts = releasedPosts.reduce((acc, item) => {
|
||||
acc[item.integrationId] = (acc[item.integrationId] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as { [key: string]: number });
|
||||
|
||||
const totalOrderItems = findOrder.ordersItems.reduce((acc, item) => {
|
||||
acc[item.integrationId] = (acc[item.integrationId] || 0) + item.quantity;
|
||||
return acc;
|
||||
}, {} as { [key: string]: number });
|
||||
|
||||
const calculate = Object.keys(totalOrderItems).reduce((acc, key) => {
|
||||
acc.push({
|
||||
price: findOrder.ordersItems.find((p) => p.integrationId === key)!
|
||||
.price,
|
||||
quantity: totalOrderItems[key] - (totalPosts[key] || 0),
|
||||
});
|
||||
return acc;
|
||||
}, [] as { price: number; quantity: number }[]);
|
||||
|
||||
const price = calculate.reduce((acc, item) => {
|
||||
acc += item.price * item.quantity;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
price,
|
||||
account: findOrder.seller.account,
|
||||
charge: findOrder.captureId,
|
||||
posts: nonReleasedPosts,
|
||||
sellerId: findOrder.seller.id,
|
||||
};
|
||||
}
|
||||
|
||||
payoutProblem(
|
||||
orderId: string,
|
||||
sellerId: string,
|
||||
amount: number,
|
||||
postId?: string
|
||||
) {
|
||||
return this._payoutProblems.model.payoutProblems.create({
|
||||
data: {
|
||||
amount,
|
||||
orderId,
|
||||
...(postId ? { postId } : {}),
|
||||
userId: sellerId,
|
||||
status: 'PAYMENT_ERROR',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOrders(userId: string, orgId: string, type: 'seller' | 'buyer') {
|
||||
const orders = await this._orders.model.orders.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['ACCEPTED', 'PENDING', 'COMPLETED'],
|
||||
},
|
||||
...(type === 'seller'
|
||||
? {
|
||||
sellerId: userId,
|
||||
}
|
||||
: {
|
||||
messageGroup: {
|
||||
buyerOrganizationId: orgId,
|
||||
},
|
||||
}),
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
...(type === 'seller'
|
||||
? {
|
||||
buyer: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
seller: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
ordersItems: {
|
||||
select: {
|
||||
id: true,
|
||||
quantity: true,
|
||||
price: true,
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
picture: true,
|
||||
name: true,
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
posts: {
|
||||
select: {
|
||||
id: true,
|
||||
integrationId: true,
|
||||
releaseURL: true,
|
||||
approvedSubmitForOrder: true,
|
||||
state: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
orders: await Promise.all(
|
||||
orders.map(async (order) => {
|
||||
return {
|
||||
id: order.id,
|
||||
status: order.status,
|
||||
// @ts-ignore
|
||||
name: type === 'seller' ? order?.buyer?.name : order?.seller?.name,
|
||||
price: order.ordersItems.reduce(
|
||||
(acc, item) => acc + item.price * item.quantity,
|
||||
0
|
||||
),
|
||||
details: await Promise.all(
|
||||
order.ordersItems.map((item) => {
|
||||
return {
|
||||
posted: order.posts.filter(
|
||||
(p) =>
|
||||
p.releaseURL && p.integrationId === item.integration.id
|
||||
).length,
|
||||
submitted: order.posts.filter(
|
||||
(p) =>
|
||||
!p.releaseURL &&
|
||||
(p.approvedSubmitForOrder === 'WAITING_CONFIRMATION' ||
|
||||
p.approvedSubmitForOrder === 'YES') &&
|
||||
p.integrationId === item.integration.id
|
||||
).length,
|
||||
integration: item.integration,
|
||||
total: item.quantity,
|
||||
price: item.price,
|
||||
};
|
||||
})
|
||||
),
|
||||
};
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getPost(userId: string, orgId: string, postId: string) {
|
||||
return this._post.model.post.findFirst({
|
||||
where: {
|
||||
id: postId,
|
||||
submittedForOrder: {
|
||||
messageGroup: {
|
||||
OR: [{ sellerId: userId }, { buyerOrganizationId: orgId }],
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
organizationId: true,
|
||||
integration: {
|
||||
select: {
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { MessagesRepository } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.repository';
|
||||
import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto';
|
||||
import { AddMessageDto } from '@gitroom/nestjs-libraries/dtos/messages/add.message';
|
||||
import { CreateOfferDto } from '@gitroom/nestjs-libraries/dtos/marketplace/create.offer.dto';
|
||||
import { From, OrderStatus, User } from '@prisma/client';
|
||||
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
|
||||
@Injectable()
|
||||
export class MessagesService {
|
||||
constructor(
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _messagesRepository: MessagesRepository,
|
||||
private _organizationRepository: OrganizationRepository,
|
||||
private _inAppNotificationService: NotificationService
|
||||
) {}
|
||||
|
||||
async createConversation(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
body: NewConversationDto
|
||||
) {
|
||||
const conversation = await this._messagesRepository.createConversation(
|
||||
userId,
|
||||
organizationId,
|
||||
body
|
||||
);
|
||||
|
||||
const orgs = await this._organizationRepository.getOrgsByUserId(body.to);
|
||||
await Promise.all(
|
||||
orgs.map(async (org) => {
|
||||
return this._inAppNotificationService.inAppNotification(
|
||||
org.id,
|
||||
'Request for service',
|
||||
'A user has requested a service from you',
|
||||
true
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
getMessagesGroup(userId: string, organizationId: string) {
|
||||
return this._messagesRepository.getMessagesGroup(userId, organizationId);
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
groupId: string,
|
||||
page: number
|
||||
) {
|
||||
if (page === 1) {
|
||||
this._messagesRepository.updateOrderOnline(userId);
|
||||
}
|
||||
|
||||
return this._messagesRepository.getMessages(
|
||||
userId,
|
||||
organizationId,
|
||||
groupId,
|
||||
page
|
||||
);
|
||||
}
|
||||
|
||||
async createNewMessage(
|
||||
group: string,
|
||||
from: From,
|
||||
content: string,
|
||||
special?: object
|
||||
) {
|
||||
const message = await this._messagesRepository.createNewMessage(
|
||||
group,
|
||||
from,
|
||||
content,
|
||||
special
|
||||
);
|
||||
|
||||
const user = from === 'BUYER' ? message.group.seller : message.group.buyer;
|
||||
|
||||
await Promise.all(
|
||||
user.organizations.map((p) => {
|
||||
return this.sendMessageNotification({
|
||||
id: p.organizationId,
|
||||
lastOnline: user.lastOnline,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
async sendMessageNotification(user: { id: string; lastOnline: Date }) {
|
||||
if (dayjs(user.lastOnline).add(5, 'minute').isBefore(dayjs())) {
|
||||
await this._inAppNotificationService.inAppNotification(
|
||||
user.id,
|
||||
'New message',
|
||||
'You have a new message',
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
groupId: string,
|
||||
body: AddMessageDto
|
||||
) {
|
||||
const message = await this._messagesRepository.createMessage(
|
||||
userId,
|
||||
orgId,
|
||||
groupId,
|
||||
body
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
message.organizations.map((p) => {
|
||||
return this.sendMessageNotification({
|
||||
id: p.organizationId,
|
||||
lastOnline: message.lastOnline,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
createOffer(userId: string, body: CreateOfferDto) {
|
||||
return this._messagesRepository.createOffer(userId, body);
|
||||
}
|
||||
|
||||
getOrderDetails(userId: string, organizationId: string, orderId: string) {
|
||||
return this._messagesRepository.getOrderDetails(
|
||||
userId,
|
||||
organizationId,
|
||||
orderId
|
||||
);
|
||||
}
|
||||
|
||||
canAddPost(id: string, order: string, integrationId: string) {
|
||||
return this._messagesRepository.canAddPost(id, order, integrationId);
|
||||
}
|
||||
|
||||
changeOrderStatus(
|
||||
orderId: string,
|
||||
status: OrderStatus,
|
||||
paymentIntent?: string
|
||||
) {
|
||||
return this._messagesRepository.changeOrderStatus(
|
||||
orderId,
|
||||
status,
|
||||
paymentIntent
|
||||
);
|
||||
}
|
||||
|
||||
getOrgByOrder(orderId: string) {
|
||||
return this._messagesRepository.getOrgByOrder(orderId);
|
||||
}
|
||||
|
||||
getMarketplaceAvailableOffers(orgId: string, id: string) {
|
||||
return this._messagesRepository.getMarketplaceAvailableOffers(orgId, id);
|
||||
}
|
||||
|
||||
getPost(userId: string, orgId: string, postId: string) {
|
||||
return this._messagesRepository.getPost(userId, orgId, postId);
|
||||
}
|
||||
|
||||
requestRevision(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
postId: string,
|
||||
message: string
|
||||
) {
|
||||
return this._messagesRepository.requestRevision(
|
||||
userId,
|
||||
orgId,
|
||||
postId,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
async requestApproved(
|
||||
userId: string,
|
||||
orgId: string,
|
||||
postId: string,
|
||||
message: string
|
||||
) {
|
||||
const post = await this._messagesRepository.requestApproved(
|
||||
userId,
|
||||
orgId,
|
||||
postId,
|
||||
message
|
||||
);
|
||||
if (post) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: post.id,
|
||||
options: {
|
||||
delay: 0, //dayjs(post.publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: post.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async requestCancel(orgId: string, postId: string) {
|
||||
const cancel = await this._messagesRepository.requestCancel(orgId, postId);
|
||||
await this._workerServiceProducer.delete('post', postId);
|
||||
return cancel;
|
||||
}
|
||||
|
||||
async completeOrderAndPay(orgId: string, order: string) {
|
||||
const orderList = await this._messagesRepository.completeOrderAndPay(
|
||||
orgId,
|
||||
order
|
||||
);
|
||||
if (!orderList) {
|
||||
return false;
|
||||
}
|
||||
orderList.posts.forEach((post) => {
|
||||
this._workerServiceProducer.delete('post', post.id);
|
||||
});
|
||||
return orderList;
|
||||
}
|
||||
|
||||
completeOrder(orderId: string) {
|
||||
return this._messagesRepository.completeOrder(orderId);
|
||||
}
|
||||
|
||||
payoutProblem(
|
||||
orderId: string,
|
||||
sellerId: string,
|
||||
amount: number,
|
||||
postId?: string
|
||||
) {
|
||||
return this._messagesRepository.payoutProblem(
|
||||
orderId,
|
||||
sellerId,
|
||||
amount,
|
||||
postId
|
||||
);
|
||||
}
|
||||
|
||||
getOrders(userId: string, orgId: string, type: 'seller' | 'buyer') {
|
||||
return this._messagesRepository.getOrders(userId, orgId, type);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
export const tagsList =
|
||||
process.env.isGeneral === 'true'
|
||||
? [
|
||||
{
|
||||
key: 'services',
|
||||
name: 'Niches',
|
||||
options: [
|
||||
{ key: 'real-estate', value: 'Real Estate' },
|
||||
{ key: 'fashion', value: 'Fashion' },
|
||||
{ key: 'health-and-fitness', value: 'Health and Fitness' },
|
||||
{ key: 'beauty', value: 'Beauty' },
|
||||
{ key: 'travel', value: 'Travel' },
|
||||
{ key: 'food', value: 'Food' },
|
||||
{ key: 'tech', value: 'Tech' },
|
||||
{ key: 'gaming', value: 'Gaming' },
|
||||
{ key: 'parenting', value: 'Parenting' },
|
||||
{ key: 'education', value: 'Education' },
|
||||
{ key: 'business', value: 'Business' },
|
||||
{ key: 'finance', value: 'Finance' },
|
||||
{ key: 'diy', value: 'DIY' },
|
||||
{ key: 'pets', value: 'Pets' },
|
||||
{ key: 'lifestyle', value: 'Lifestyle' },
|
||||
{ key: 'sports', value: 'Sports' },
|
||||
{ key: 'entertainment', value: 'Entertainment' },
|
||||
{ key: 'art', value: 'Art' },
|
||||
{ key: 'photography', value: 'Photography' },
|
||||
{ key: 'sustainability', value: 'Sustainability' },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'services',
|
||||
name: 'Services',
|
||||
options: [
|
||||
{
|
||||
key: 'content-writer',
|
||||
value: 'Content Writer',
|
||||
},
|
||||
{
|
||||
key: 'influencers',
|
||||
value: 'Influencers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'niches',
|
||||
name: 'Niches',
|
||||
options: [
|
||||
{
|
||||
key: 'kubernetes',
|
||||
value: 'Kubernetes',
|
||||
},
|
||||
{
|
||||
key: 'fullstack',
|
||||
value: 'Fullstack',
|
||||
},
|
||||
{
|
||||
key: 'security',
|
||||
value: 'Security',
|
||||
},
|
||||
{
|
||||
key: 'infrastructure',
|
||||
value: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
key: 'productivity',
|
||||
value: 'Productivity',
|
||||
},
|
||||
{
|
||||
key: 'web3',
|
||||
value: 'Web3',
|
||||
},
|
||||
{
|
||||
key: 'cloud-native',
|
||||
value: 'Cloud Native',
|
||||
},
|
||||
{
|
||||
key: 'ml',
|
||||
value: 'ML',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'technologies',
|
||||
name: 'Technologies',
|
||||
options: [
|
||||
{
|
||||
key: 'html',
|
||||
value: 'HTML',
|
||||
},
|
||||
{
|
||||
key: 'css',
|
||||
value: 'CSS',
|
||||
},
|
||||
{
|
||||
key: 'javascript',
|
||||
value: 'JavaScript',
|
||||
},
|
||||
{
|
||||
key: 'typescript',
|
||||
value: 'TypeScript',
|
||||
},
|
||||
{
|
||||
key: 'rust',
|
||||
value: 'Rust',
|
||||
},
|
||||
{
|
||||
key: 'go',
|
||||
value: 'Go',
|
||||
},
|
||||
{
|
||||
key: 'python',
|
||||
value: 'Python',
|
||||
},
|
||||
{
|
||||
key: 'java',
|
||||
value: 'Java',
|
||||
},
|
||||
{
|
||||
key: 'php',
|
||||
value: 'PHP',
|
||||
},
|
||||
{
|
||||
key: 'ruby',
|
||||
value: 'Ruby',
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
value: 'C/C++',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const allTagsOptions = tagsList.reduce((acc, tag) => {
|
||||
return [...acc, ...tag.options];
|
||||
}, [] as Array<{ key: string; value: string }>);
|
||||
|
|
@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
|
|||
import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository';
|
||||
import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
|
||||
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { TemporalService } from 'nestjs-temporal-core';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import { organizationId } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
export type NotificationType = 'success' | 'fail' | 'info';
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ export class NotificationService {
|
|||
private _notificationRepository: NotificationsRepository,
|
||||
private _emailService: EmailService,
|
||||
private _organizationRepository: OrganizationRepository,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
private _temporalService: TemporalService
|
||||
) {}
|
||||
|
||||
getMainPageCount(organizationId: string, userId: string) {
|
||||
|
|
@ -31,13 +31,6 @@ export class NotificationService {
|
|||
);
|
||||
}
|
||||
|
||||
getNotificationsSince(organizationId: string, since: string) {
|
||||
return this._notificationRepository.getNotificationsSince(
|
||||
organizationId,
|
||||
since
|
||||
);
|
||||
}
|
||||
|
||||
async inAppNotification(
|
||||
orgId: string,
|
||||
subject: string,
|
||||
|
|
@ -46,42 +39,37 @@ export class NotificationService {
|
|||
digest = false,
|
||||
type: NotificationType = 'success'
|
||||
) {
|
||||
const date = new Date().toISOString();
|
||||
await this._notificationRepository.createNotification(orgId, message);
|
||||
if (!sendEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (digest) {
|
||||
await ioRedis.watch('digest_' + orgId);
|
||||
const value = await ioRedis.get('digest_' + orgId);
|
||||
|
||||
// Track notification types in the digest
|
||||
const typesKey = 'digest_types_' + orgId;
|
||||
await ioRedis.sadd(typesKey, type);
|
||||
await ioRedis.expire(typesKey, 120); // Slightly longer than digest window
|
||||
|
||||
if (value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ioRedis
|
||||
.multi()
|
||||
.set('digest_' + orgId, date)
|
||||
.expire('digest_' + orgId, 60)
|
||||
.exec();
|
||||
|
||||
this._workerServiceProducer.emit('sendDigestEmail', {
|
||||
id: 'digest_' + orgId,
|
||||
options: {
|
||||
delay: 60000,
|
||||
},
|
||||
payload: {
|
||||
subject,
|
||||
org: orgId,
|
||||
since: date,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.signalWithStart('digestEmailWorkflow', {
|
||||
workflowId: 'digest_email_workflow_' + orgId,
|
||||
signal: 'email',
|
||||
signalArgs: [
|
||||
[
|
||||
{
|
||||
title: subject,
|
||||
message,
|
||||
type,
|
||||
},
|
||||
],
|
||||
],
|
||||
taskQueue: 'main',
|
||||
args: [{ organizationId: orgId }],
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: organizationId,
|
||||
value: orgId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
} catch (err) {}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -111,45 +99,6 @@ export class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
async getDigestTypes(orgId: string): Promise<NotificationType[]> {
|
||||
const typesKey = 'digest_types_' + orgId;
|
||||
const types = await ioRedis.smembers(typesKey);
|
||||
// Clean up the types key after reading
|
||||
await ioRedis.del(typesKey);
|
||||
return types as NotificationType[];
|
||||
}
|
||||
|
||||
async sendDigestEmailsToOrg(
|
||||
orgId: string,
|
||||
subject: string,
|
||||
message: string,
|
||||
types: NotificationType[]
|
||||
) {
|
||||
const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId);
|
||||
const hasInfo = types.includes('info');
|
||||
const hasSuccess = types.includes('success');
|
||||
const hasFail = types.includes('fail');
|
||||
|
||||
for (const user of userOrg?.users || []) {
|
||||
// 'info' type is always sent regardless of preferences
|
||||
if (hasInfo) {
|
||||
await this.sendEmail(user.user.email, subject, message);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For digest, check if user wants any of the notification types in the digest
|
||||
const wantsSuccess = hasSuccess && user.user.sendSuccessEmails;
|
||||
const wantsFail = hasFail && user.user.sendFailureEmails;
|
||||
|
||||
// Only send if user wants at least one type of notification in the digest
|
||||
if (!wantsSuccess && !wantsFail) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.sendEmail(user.user.email, subject, message);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
|
||||
await this._emailService.sendEmail(to, subject, html, replyTo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@ export class OrganizationRepository {
|
|||
select: {
|
||||
email: true,
|
||||
id: true,
|
||||
sendSuccessEmails: true,
|
||||
sendFailureEmails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,24 +27,6 @@ export class PostsRepository {
|
|||
private _errors: PrismaRepository<'errors'>
|
||||
) {}
|
||||
|
||||
checkPending15minutesBack() {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
publishDate: {
|
||||
lte: dayjs.utc().subtract(15, 'minute').toDate(),
|
||||
gte: dayjs.utc().subtract(30, 'minute').toDate(),
|
||||
},
|
||||
state: 'QUEUE',
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
publishDate: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
searchForMissingThreeHoursPosts() {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
|
|
@ -54,8 +36,8 @@ export class PostsRepository {
|
|||
disabled: false,
|
||||
},
|
||||
publishDate: {
|
||||
gte: dayjs.utc().toDate(),
|
||||
lt: dayjs.utc().add(3, 'hour').toDate(),
|
||||
gte: dayjs.utc().subtract(2, 'hour').toDate(),
|
||||
lt: dayjs.utc().add(2, 'hour').toDate(),
|
||||
},
|
||||
state: 'QUEUE',
|
||||
deletedAt: null,
|
||||
|
|
@ -63,6 +45,12 @@ export class PostsRepository {
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
integration: {
|
||||
select: {
|
||||
providerIdentifier: true,
|
||||
}
|
||||
},
|
||||
publishDate: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -694,6 +682,32 @@ export class PostsRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async getPostByForWebhookId(postId: string) {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
id: postId,
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
publishDate: true,
|
||||
releaseURL: true,
|
||||
state: true,
|
||||
integration: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
providerIdentifier: true,
|
||||
picture: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getPostsSince(orgId: string, since: string) {
|
||||
return this._post.model.post.findMany({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
|
|
@ -8,38 +7,29 @@ import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts
|
|||
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
|
||||
import dayjs from 'dayjs';
|
||||
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { Integration, Post, Media, From } from '@prisma/client';
|
||||
import { Integration, Post, Media, From, State } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { capitalize, shuffle, uniq } from 'lodash';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { shuffle } from 'lodash';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import {
|
||||
BadBody,
|
||||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
|
||||
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
|
||||
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
|
||||
import axios from 'axios';
|
||||
import sharp from 'sharp';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { Readable } from 'stream';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
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';
|
||||
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
|
||||
import { TemporalService } from 'nestjs-temporal-core';
|
||||
import { TypedSearchAttributes } from '@temporalio/common';
|
||||
import {
|
||||
organizationId,
|
||||
postId as postIdSearchParam,
|
||||
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
|
|
@ -51,26 +41,22 @@ export class PostsService {
|
|||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _postRepository: PostsRepository,
|
||||
private _workerServiceProducer: BullMqClient,
|
||||
private _integrationManager: IntegrationManager,
|
||||
private _notificationService: NotificationService,
|
||||
private _messagesService: MessagesService,
|
||||
private _stripeService: StripeService,
|
||||
private _integrationService: IntegrationService,
|
||||
private _mediaService: MediaService,
|
||||
private _shortLinkService: ShortLinkService,
|
||||
private _webhookService: WebhooksService,
|
||||
private openaiService: OpenaiService,
|
||||
private _refreshIntegrationService: RefreshIntegrationService
|
||||
private _openaiService: OpenaiService,
|
||||
private _temporalService: TemporalService
|
||||
) {}
|
||||
|
||||
checkPending15minutesBack() {
|
||||
return this._postRepository.checkPending15minutesBack();
|
||||
}
|
||||
searchForMissingThreeHoursPosts() {
|
||||
return this._postRepository.searchForMissingThreeHoursPosts();
|
||||
}
|
||||
|
||||
updatePost(id: string, postId: string, releaseURL: string) {
|
||||
return this._postRepository.updatePost(id, postId, releaseURL);
|
||||
}
|
||||
|
||||
async getStatistics(orgId: string, id: string) {
|
||||
const getPost = await this.getPostsRecursively(id, true, orgId, true);
|
||||
const content = getPost.map((p) => p.content);
|
||||
|
|
@ -292,102 +278,7 @@ export class PostsService {
|
|||
return this._postRepository.getOldPosts(orgId, date);
|
||||
}
|
||||
|
||||
async post(id: string) {
|
||||
const allPosts = await this.getPostsRecursively(id, true);
|
||||
const [firstPost, ...morePosts] = allPosts;
|
||||
if (!firstPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstPost.integration?.refreshNeeded) {
|
||||
await this._notificationService.inAppNotification(
|
||||
firstPost.organizationId,
|
||||
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
|
||||
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name} because you need to reconnect it. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstPost.integration?.disabled) {
|
||||
await this._notificationService.inAppNotification(
|
||||
firstPost.organizationId,
|
||||
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
|
||||
`We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name} because it's disabled. Please enable it and try again.`,
|
||||
true,
|
||||
false,
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const finalPost = await this.postSocial(firstPost.integration!, [
|
||||
firstPost,
|
||||
...morePosts,
|
||||
]);
|
||||
|
||||
if (firstPost?.intervalInDays) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id,
|
||||
options: {
|
||||
delay: firstPost.intervalInDays * 86400000,
|
||||
},
|
||||
payload: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!finalPost?.postId || !finalPost?.releaseURL) {
|
||||
await this._postRepository.changeState(firstPost.id, 'ERROR');
|
||||
await this._notificationService.inAppNotification(
|
||||
firstPost.organizationId,
|
||||
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
|
||||
`An error occurred while posting on ${firstPost.integration?.providerIdentifier}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
await this._postRepository.changeState(
|
||||
firstPost.id,
|
||||
'ERROR',
|
||||
err,
|
||||
allPosts
|
||||
);
|
||||
if (err instanceof BadBody) {
|
||||
await this._notificationService.inAppNotification(
|
||||
firstPost.organizationId,
|
||||
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
|
||||
`An error occurred while posting on ${
|
||||
firstPost.integration?.providerIdentifier
|
||||
}${err?.message ? `: ${err?.message}` : ``}`,
|
||||
true,
|
||||
false,
|
||||
'fail'
|
||||
);
|
||||
|
||||
console.error(
|
||||
'[Error] posting on',
|
||||
firstPost.integration?.providerIdentifier,
|
||||
err.identifier,
|
||||
err.json,
|
||||
err.body,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTags(orgId: string, post: Post[]): Promise<Post[]> {
|
||||
public async updateTags(orgId: string, post: Post[]): Promise<Post[]> {
|
||||
const plainText = JSON.stringify(post);
|
||||
const extract = Array.from(
|
||||
plainText.match(/\(post:[a-zA-Z0-9-_]+\)/g) || []
|
||||
|
|
@ -411,127 +302,7 @@ export class PostsService {
|
|||
return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]);
|
||||
}
|
||||
|
||||
private async postSocial(
|
||||
integration: Integration,
|
||||
posts: Post[],
|
||||
forceRefresh = false
|
||||
): Promise<Partial<{ postId: string; releaseURL: string }> | undefined> {
|
||||
const getIntegration = this._integrationManager.getSocialIntegration(
|
||||
integration.providerIdentifier
|
||||
);
|
||||
|
||||
if (!getIntegration) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (dayjs(integration?.tokenExpiration).isBefore(dayjs()) || forceRefresh) {
|
||||
const data = await this._refreshIntegrationService.refresh(integration);
|
||||
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
integration.token = data.accessToken;
|
||||
|
||||
if (getIntegration.refreshWait) {
|
||||
await timer(10000);
|
||||
}
|
||||
}
|
||||
|
||||
const newPosts = await this.updateTags(integration.organizationId, posts);
|
||||
|
||||
try {
|
||||
const publishedPosts = await getIntegration.post(
|
||||
integration.internalId,
|
||||
integration.token,
|
||||
await Promise.all(
|
||||
(newPosts || []).map(async (p) => ({
|
||||
id: p.id,
|
||||
message: stripHtmlValidation(
|
||||
getIntegration.editor,
|
||||
p.content,
|
||||
true,
|
||||
false,
|
||||
!/<\/?[a-z][\s\S]*>/i.test(p.content),
|
||||
getIntegration.mentionFormat
|
||||
),
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
media: await this.updateMedia(
|
||||
p.id,
|
||||
JSON.parse(p.image || '[]'),
|
||||
getIntegration?.convertToJPEG || false
|
||||
),
|
||||
}))
|
||||
),
|
||||
integration
|
||||
);
|
||||
|
||||
for (const post of publishedPosts) {
|
||||
try {
|
||||
await this._postRepository.updatePost(
|
||||
post.id,
|
||||
post.postId,
|
||||
post.releaseURL
|
||||
);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await this._notificationService.inAppNotification(
|
||||
integration.organizationId,
|
||||
`Your post has been published on ${capitalize(
|
||||
integration.providerIdentifier
|
||||
)}`,
|
||||
`Your post has been published on ${capitalize(
|
||||
integration.providerIdentifier
|
||||
)} at ${publishedPosts[0].releaseURL}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
await this._webhookService.digestWebhooks(
|
||||
integration.organizationId,
|
||||
dayjs(newPosts[0].publishDate).format('YYYY-MM-DDTHH:mm:00')
|
||||
);
|
||||
|
||||
await this.checkPlugs(
|
||||
integration.organizationId,
|
||||
getIntegration.identifier,
|
||||
integration.id,
|
||||
publishedPosts[0].postId
|
||||
);
|
||||
|
||||
await this.checkInternalPlug(
|
||||
integration,
|
||||
integration.organizationId,
|
||||
publishedPosts[0].postId,
|
||||
JSON.parse(newPosts[0].settings || '{}')
|
||||
);
|
||||
} catch (err) {}
|
||||
|
||||
return {
|
||||
postId: publishedPosts[0].postId,
|
||||
releaseURL: publishedPosts[0].releaseURL,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof RefreshToken) {
|
||||
return this.postSocial(integration, posts, true);
|
||||
}
|
||||
|
||||
if (err instanceof BadBody) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new BadBody(
|
||||
integration.providerIdentifier,
|
||||
JSON.stringify(err),
|
||||
{} as any,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkInternalPlug(
|
||||
public async checkInternalPlug(
|
||||
integration: Integration,
|
||||
orgId: string,
|
||||
id: string,
|
||||
|
|
@ -542,7 +313,7 @@ export class PostsService {
|
|||
});
|
||||
|
||||
if (plugs.length === 0) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsePlugs = plugs.reduce((all, [key, value]) => {
|
||||
|
|
@ -559,32 +330,24 @@ export class PostsService {
|
|||
active: boolean;
|
||||
}[] = Object.values(parsePlugs);
|
||||
|
||||
for (const trigger of list || []) {
|
||||
for (const int of trigger?.integrations || []) {
|
||||
this._workerServiceProducer.emit('internal-plugs', {
|
||||
id: 'plug_' + id + '_' + trigger.name + '_' + int.id,
|
||||
options: {
|
||||
delay: +trigger.delay,
|
||||
},
|
||||
payload: {
|
||||
post: id,
|
||||
originalIntegration: integration.id,
|
||||
integration: int.id,
|
||||
plugName: trigger.name,
|
||||
orgId: orgId,
|
||||
delay: +trigger.delay,
|
||||
information: trigger,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return (list || []).flatMap((trigger) => {
|
||||
return (trigger?.integrations || []).flatMap((int) => ({
|
||||
type: 'internal-plug',
|
||||
post: id,
|
||||
originalIntegration: integration.id,
|
||||
integration: int.id,
|
||||
plugName: trigger.name,
|
||||
orgId: orgId,
|
||||
delay: +trigger.delay,
|
||||
information: trigger,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async checkPlugs(
|
||||
public async checkPlugs(
|
||||
orgId: string,
|
||||
providerName: string,
|
||||
integrationId: string,
|
||||
postId: string
|
||||
integrationId: string
|
||||
) {
|
||||
const loadAllPlugs = this._integrationManager.getAllPlugs();
|
||||
const getPlugs = await this._integrationService.getPlugs(
|
||||
|
|
@ -594,35 +357,51 @@ export class PostsService {
|
|||
|
||||
const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
|
||||
|
||||
for (const plug of getPlugs) {
|
||||
const runPlug = currentPlug?.plugs?.find(
|
||||
(p: any) => p.methodName === plug.plugFunction
|
||||
)!;
|
||||
if (!runPlug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._workerServiceProducer.emit('plugs', {
|
||||
id: 'plug_' + postId + '_' + runPlug.identifier,
|
||||
options: {
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
},
|
||||
payload: {
|
||||
return getPlugs
|
||||
.filter((plug) => {
|
||||
return currentPlug?.plugs?.some(
|
||||
(p: any) => p.methodName === plug.plugFunction
|
||||
);
|
||||
})
|
||||
.map((plug) => {
|
||||
const runPlug = currentPlug?.plugs?.find(
|
||||
(p: any) => p.methodName === plug.plugFunction
|
||||
)!;
|
||||
return {
|
||||
type: 'global',
|
||||
plugId: plug.id,
|
||||
postId,
|
||||
delay: runPlug.runEveryMilliseconds,
|
||||
totalRuns: runPlug.totalRuns,
|
||||
currentRun: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async deletePost(orgId: string, group: string) {
|
||||
const post = await this._postRepository.deletePost(orgId, group);
|
||||
|
||||
if (post?.id) {
|
||||
await this._workerServiceProducer.delete('post', post.id);
|
||||
return { id: post.id };
|
||||
try {
|
||||
const workflows = this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.list({
|
||||
query: `WorkflowType="postWorkflow" AND postId="${post.id}" AND ExecutionStatus="Running"`,
|
||||
});
|
||||
|
||||
for await (const executionInfo of workflows) {
|
||||
try {
|
||||
const workflow =
|
||||
await this._temporalService.client.getWorkflowHandle(
|
||||
executionInfo.workflowId
|
||||
);
|
||||
if (
|
||||
workflow &&
|
||||
(await workflow.describe()).status.name !== 'TERMINATED'
|
||||
) {
|
||||
await workflow.terminate();
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return { error: true };
|
||||
|
|
@ -632,6 +411,9 @@ export class PostsService {
|
|||
return this._postRepository.countPostsFromDay(orgId, date);
|
||||
}
|
||||
|
||||
getPostByForWebhookId(id: string) {
|
||||
return this._postRepository.getPostByForWebhookId(id);
|
||||
}
|
||||
async createPost(orgId: string, body: CreatePostDto): Promise<any[]> {
|
||||
const postList = [];
|
||||
for (const post of body.posts) {
|
||||
|
|
@ -645,48 +427,65 @@ export class PostsService {
|
|||
content: updateContent[i],
|
||||
}));
|
||||
|
||||
const { previousPost, posts } =
|
||||
await this._postRepository.createOrUpdatePost(
|
||||
body.type,
|
||||
orgId,
|
||||
body.type === 'now'
|
||||
? dayjs().format('YYYY-MM-DDTHH:mm:00')
|
||||
: body.date,
|
||||
post,
|
||||
body.tags,
|
||||
body.inter
|
||||
);
|
||||
const { posts } = await this._postRepository.createOrUpdatePost(
|
||||
body.type,
|
||||
orgId,
|
||||
body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date,
|
||||
post,
|
||||
body.tags,
|
||||
body.inter
|
||||
);
|
||||
|
||||
if (!posts?.length) {
|
||||
return [] as any[];
|
||||
}
|
||||
|
||||
await this._workerServiceProducer.delete(
|
||||
'post',
|
||||
previousPost ? previousPost : posts?.[0]?.id
|
||||
);
|
||||
try {
|
||||
const workflows = this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.list({
|
||||
query: `WorkflowType="postWorkflow" AND postId="${posts[0].id}" AND ExecutionStatus="Running"`,
|
||||
});
|
||||
|
||||
if (
|
||||
body.type === 'now' ||
|
||||
(body.type === 'schedule' && dayjs(body.date).isAfter(dayjs()))
|
||||
) {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: posts[0].id,
|
||||
options: {
|
||||
delay:
|
||||
body.type === 'now'
|
||||
? 0
|
||||
: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: posts[0].id,
|
||||
delay:
|
||||
body.type === 'now'
|
||||
? 0
|
||||
: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
for await (const executionInfo of workflows) {
|
||||
try {
|
||||
const workflow =
|
||||
await this._temporalService.client.getWorkflowHandle(
|
||||
executionInfo.workflowId
|
||||
);
|
||||
if (
|
||||
workflow &&
|
||||
(await workflow.describe()).status.name !== 'TERMINATED'
|
||||
) {
|
||||
await workflow.terminate();
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
workflowId: `post_${posts[0].id}`,
|
||||
taskQueue: 'main',
|
||||
args: [
|
||||
{
|
||||
taskQueue: post.settings.__type.split('-')[0].toLowerCase(),
|
||||
postId: posts[0].id,
|
||||
organizationId: orgId,
|
||||
},
|
||||
],
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: posts[0].id,
|
||||
},
|
||||
{
|
||||
key: organizationId,
|
||||
value: orgId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
Sentry.metrics.count('post_created', 1);
|
||||
postList.push({
|
||||
|
|
@ -699,103 +498,66 @@ export class PostsService {
|
|||
}
|
||||
|
||||
async separatePosts(content: string, len: number) {
|
||||
return this.openaiService.separatePosts(content, len);
|
||||
return this._openaiService.separatePosts(content, len);
|
||||
}
|
||||
|
||||
async changeState(id: string, state: State, err?: any, body?: any) {
|
||||
return this._postRepository.changeState(id, state, err, body);
|
||||
}
|
||||
|
||||
async changeDate(orgId: string, id: string, date: string) {
|
||||
const getPostById = await this._postRepository.getPostById(id, orgId);
|
||||
|
||||
await this._workerServiceProducer.delete('post', id);
|
||||
if (getPostById?.state !== 'DRAFT') {
|
||||
this._workerServiceProducer.emit('post', {
|
||||
id: id,
|
||||
options: {
|
||||
delay: dayjs(date).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
payload: {
|
||||
id: id,
|
||||
delay: dayjs(date).diff(dayjs(), 'millisecond'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return this._postRepository.changeDate(orgId, id, date);
|
||||
}
|
||||
|
||||
async payout(id: string, url: string) {
|
||||
const getPost = await this._postRepository.getPostById(id);
|
||||
if (!getPost || !getPost.submittedForOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const findPrice = getPost.submittedForOrder.ordersItems.find(
|
||||
(orderItem) => orderItem.integrationId === getPost.integrationId
|
||||
)!;
|
||||
|
||||
await this._messagesService.createNewMessage(
|
||||
getPost.submittedForOrder.messageGroupId,
|
||||
From.SELLER,
|
||||
'',
|
||||
{
|
||||
type: 'published',
|
||||
data: {
|
||||
id: getPost.submittedForOrder.id,
|
||||
postId: id,
|
||||
status: 'PUBLISHED',
|
||||
integrationId: getPost.integrationId,
|
||||
integration: getPost.integration.providerIdentifier,
|
||||
picture: getPost.integration.picture,
|
||||
name: getPost.integration.name,
|
||||
url,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const totalItems = getPost.submittedForOrder.ordersItems.reduce(
|
||||
(all, p) => all + p.quantity,
|
||||
0
|
||||
);
|
||||
const totalPosts = getPost.submittedForOrder.posts.length;
|
||||
|
||||
if (totalItems === totalPosts) {
|
||||
await this._messagesService.completeOrder(getPost.submittedForOrder.id);
|
||||
await this._messagesService.createNewMessage(
|
||||
getPost.submittedForOrder.messageGroupId,
|
||||
From.SELLER,
|
||||
'',
|
||||
{
|
||||
type: 'order-completed',
|
||||
data: {
|
||||
id: getPost.submittedForOrder.id,
|
||||
postId: id,
|
||||
status: 'PUBLISHED',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
const newDate = await this._postRepository.changeDate(orgId, id, date);
|
||||
|
||||
try {
|
||||
await this._stripeService.payout(
|
||||
getPost.submittedForOrder.id,
|
||||
getPost.submittedForOrder.captureId!,
|
||||
getPost.submittedForOrder.seller.account!,
|
||||
findPrice.price
|
||||
);
|
||||
const workflows = this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.list({
|
||||
query: `WorkflowType="postWorkflow" AND postId="${getPostById.id}" AND ExecutionStatus="Running"`,
|
||||
});
|
||||
|
||||
return this._notificationService.inAppNotification(
|
||||
getPost.integration.organizationId,
|
||||
'Payout completed',
|
||||
`You have received a payout of $${findPrice.price}`,
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
await this._messagesService.payoutProblem(
|
||||
getPost.submittedForOrder.id,
|
||||
getPost.submittedForOrder.seller.id,
|
||||
findPrice.price,
|
||||
id
|
||||
);
|
||||
}
|
||||
for await (const executionInfo of workflows) {
|
||||
try {
|
||||
const workflow = await this._temporalService.client.getWorkflowHandle(
|
||||
executionInfo.workflowId
|
||||
);
|
||||
if (
|
||||
workflow &&
|
||||
(await workflow.describe()).status.name !== 'TERMINATED'
|
||||
) {
|
||||
await workflow.terminate();
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
await this._temporalService.client
|
||||
.getRawClient()
|
||||
?.workflow.start('postWorkflow', {
|
||||
workflowId: `post_${getPostById.id}`,
|
||||
taskQueue: 'main',
|
||||
args: [
|
||||
{
|
||||
taskQueue: getPostById.integration.providerIdentifier
|
||||
.split('-')[0]
|
||||
.toLowerCase(),
|
||||
postId: getPostById.id,
|
||||
organizationId: orgId,
|
||||
},
|
||||
],
|
||||
typedSearchAttributes: new TypedSearchAttributes([
|
||||
{
|
||||
key: postIdSearchParam,
|
||||
value: getPostById.id,
|
||||
},
|
||||
{
|
||||
key: organizationId,
|
||||
value: orgId,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
return newDate;
|
||||
}
|
||||
|
||||
async generatePostsDraft(orgId: string, body: CreateGeneratedPostsDto) {
|
||||
|
|
@ -959,28 +721,4 @@ export class PostsService {
|
|||
) {
|
||||
return this._postRepository.createComment(orgId, userId, postId, comment);
|
||||
}
|
||||
|
||||
async sendDigestEmail(subject: string, orgId: string, since: string) {
|
||||
const getNotificationsForOrgSince =
|
||||
await this._notificationService.getNotificationsSince(orgId, since);
|
||||
if (getNotificationsForOrgSince.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the types of notifications in this digest
|
||||
const types = await this._notificationService.getDigestTypes(orgId);
|
||||
|
||||
const message = getNotificationsForOrgSince
|
||||
.map((p) => p.content)
|
||||
.join('<br />');
|
||||
|
||||
await this._notificationService.sendDigestEmailsToOrg(
|
||||
orgId,
|
||||
getNotificationsForOrgSince.length === 1
|
||||
? subject
|
||||
: '[Postiz] Your latest notifications',
|
||||
message,
|
||||
types.length > 0 ? types : ['success'] // Default to success if no types tracked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ model User {
|
|||
lastReadNotifications DateTime @default(now())
|
||||
inviteId String?
|
||||
activated Boolean @default(true)
|
||||
marketplace Boolean @default(true)
|
||||
account String?
|
||||
connectedAccount Boolean @default(false)
|
||||
lastOnline DateTime @default(now())
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto';
|
||||
|
||||
@Injectable()
|
||||
export class StarsRepository {
|
||||
constructor(
|
||||
private _github: PrismaRepository<'gitHub'>,
|
||||
private _stars: PrismaRepository<'star'>,
|
||||
private _trending: PrismaRepository<'trending'>
|
||||
) {}
|
||||
getGitHubRepositoriesByOrgId(org: string) {
|
||||
return this._github.model.gitHub.findMany({
|
||||
where: {
|
||||
organizationId: org,
|
||||
},
|
||||
});
|
||||
}
|
||||
replaceOrAddTrending(
|
||||
language: string,
|
||||
hashedNames: string,
|
||||
arr: { name: string; position: number }[]
|
||||
) {
|
||||
return this._trending.model.trending.upsert({
|
||||
create: {
|
||||
language,
|
||||
hash: hashedNames,
|
||||
trendingList: JSON.stringify(arr),
|
||||
date: new Date(),
|
||||
},
|
||||
update: {
|
||||
language,
|
||||
hash: hashedNames,
|
||||
trendingList: JSON.stringify(arr),
|
||||
date: new Date(),
|
||||
},
|
||||
where: {
|
||||
language,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAllGitHubRepositories() {
|
||||
return this._github.model.gitHub.findMany({
|
||||
distinct: ['login'],
|
||||
});
|
||||
}
|
||||
|
||||
async getLastStarsByLogin(login: string) {
|
||||
return (
|
||||
await this._stars.model.star.findMany({
|
||||
where: {
|
||||
login,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
})
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
async getStarsByLogin(login: string) {
|
||||
return this._stars.model.star.findMany({
|
||||
where: {
|
||||
login,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getGitHubsByNames(names: string[]) {
|
||||
return this._github.model.gitHub.findMany({
|
||||
where: {
|
||||
login: {
|
||||
in: names,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findValidToken(login: string) {
|
||||
return this._github.model.gitHub.findFirst({
|
||||
where: {
|
||||
login,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createStars(
|
||||
login: string,
|
||||
totalNewsStars: number,
|
||||
totalStars: number,
|
||||
totalNewForks: number,
|
||||
totalForks: number,
|
||||
date: Date
|
||||
) {
|
||||
return this._stars.model.star.upsert({
|
||||
create: {
|
||||
login,
|
||||
stars: totalNewsStars,
|
||||
forks: totalNewForks,
|
||||
totalForks,
|
||||
totalStars,
|
||||
date,
|
||||
},
|
||||
update: {
|
||||
stars: totalNewsStars,
|
||||
totalStars,
|
||||
forks: totalNewForks,
|
||||
totalForks,
|
||||
},
|
||||
where: {
|
||||
login_date: {
|
||||
date,
|
||||
login,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getTrendingByLanguage(language: string) {
|
||||
return this._trending.model.trending.findUnique({
|
||||
where: {
|
||||
language,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getStarsFilter(githubs: string[], starsFilter: StarsListDto) {
|
||||
return this._stars.model.star.findMany({
|
||||
orderBy: {
|
||||
[starsFilter.key || 'date']: starsFilter.state || 'desc',
|
||||
},
|
||||
where: {
|
||||
login: {
|
||||
in: githubs.filter((f) => f),
|
||||
},
|
||||
},
|
||||
take: 20,
|
||||
skip: (starsFilter.page - 1) * 10,
|
||||
});
|
||||
}
|
||||
|
||||
addGitHub(orgId: string, accessToken: string) {
|
||||
return this._github.model.gitHub.create({
|
||||
data: {
|
||||
token: accessToken,
|
||||
organizationId: orgId,
|
||||
jobId: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getGitHubById(orgId: string, id: string) {
|
||||
return this._github.model.gitHub.findUnique({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateGitHubLogin(orgId: string, id: string, login: string) {
|
||||
return this._github.model.gitHub.update({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
login,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteRepository(orgId: string, id: string) {
|
||||
return this._github.model.gitHub.delete({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizationsByGitHubLogin(login: string) {
|
||||
return this._github.model.gitHub.findMany({
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
where: {
|
||||
login,
|
||||
},
|
||||
distinct: ['organizationId'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,485 +0,0 @@
|
|||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
import { StarsRepository } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.repository';
|
||||
import { chunk, groupBy } from 'lodash';
|
||||
import dayjs from 'dayjs';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { StarsListDto } from '@gitroom/nestjs-libraries/dtos/analytics/stars.list.dto';
|
||||
import { mean } from 'simple-statistics';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
enum Inform {
|
||||
Removed,
|
||||
New,
|
||||
Changed,
|
||||
}
|
||||
@Injectable()
|
||||
export class StarsService {
|
||||
constructor(
|
||||
private _starsRepository: StarsRepository,
|
||||
private _notificationsService: NotificationService,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
|
||||
getGitHubRepositoriesByOrgId(org: string) {
|
||||
return this._starsRepository.getGitHubRepositoriesByOrgId(org);
|
||||
}
|
||||
|
||||
getAllGitHubRepositories() {
|
||||
return this._starsRepository.getAllGitHubRepositories();
|
||||
}
|
||||
|
||||
getStarsByLogin(login: string) {
|
||||
return this._starsRepository.getStarsByLogin(login);
|
||||
}
|
||||
|
||||
getLastStarsByLogin(login: string) {
|
||||
return this._starsRepository.getLastStarsByLogin(login);
|
||||
}
|
||||
|
||||
createStars(
|
||||
login: string,
|
||||
totalNewsStars: number,
|
||||
totalStars: number,
|
||||
totalNewForks: number,
|
||||
totalForks: number,
|
||||
date: Date
|
||||
) {
|
||||
return this._starsRepository.createStars(
|
||||
login,
|
||||
totalNewsStars,
|
||||
totalStars,
|
||||
totalNewForks,
|
||||
totalForks,
|
||||
date
|
||||
);
|
||||
}
|
||||
|
||||
async sync(login: string, token?: string) {
|
||||
const loadAllStars = await this.syncProcess(login, token);
|
||||
const loadAllForks = await this.syncForksProcess(login, token);
|
||||
|
||||
const allDates = [
|
||||
...new Set([...Object.keys(loadAllStars), ...Object.keys(loadAllForks)]),
|
||||
];
|
||||
|
||||
const sortedArray = allDates.sort(
|
||||
(a, b) => dayjs(a).unix() - dayjs(b).unix()
|
||||
);
|
||||
|
||||
let addPreviousStars = 0;
|
||||
let addPreviousForks = 0;
|
||||
for (const date of sortedArray) {
|
||||
const dateObject = dayjs(date).toDate();
|
||||
addPreviousStars += loadAllStars[date] || 0;
|
||||
addPreviousForks += loadAllForks[date] || 0;
|
||||
|
||||
await this._starsRepository.createStars(
|
||||
login,
|
||||
loadAllStars[date] || 0,
|
||||
addPreviousStars,
|
||||
loadAllForks[date] || 0,
|
||||
addPreviousForks,
|
||||
dateObject
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async findValidToken(login: string) {
|
||||
return this._starsRepository.findValidToken(login);
|
||||
}
|
||||
|
||||
async fetchWillFallback(url: string, userToken?: string): Promise<Response> {
|
||||
if (userToken) {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3.star+json',
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
const response2 = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3.star+json',
|
||||
...(process.env.GITHUB_AUTH
|
||||
? { Authorization: `token ${process.env.GITHUB_AUTH}` }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const totalRemaining = +(
|
||||
response2.headers.get('x-ratelimit-remaining') ||
|
||||
response2.headers.get('X-RateLimit-Remaining') ||
|
||||
0
|
||||
);
|
||||
const resetTime = +(
|
||||
response2.headers.get('x-ratelimit-reset') ||
|
||||
response2.headers.get('X-RateLimit-Reset') ||
|
||||
0
|
||||
);
|
||||
|
||||
if (totalRemaining < 10) {
|
||||
console.log('waiting for the rate limit');
|
||||
const delay = resetTime * 1000 - Date.now() + 1000;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
return this.fetchWillFallback(url, userToken);
|
||||
}
|
||||
|
||||
return response2;
|
||||
}
|
||||
|
||||
async syncForksProcess(login: string, userToken?: string, page = 1) {
|
||||
console.log('processing forks');
|
||||
const starsRequest = await this.fetchWillFallback(
|
||||
`https://api.github.com/repos/${login}/forks?page=${page}&per_page=100`,
|
||||
userToken
|
||||
);
|
||||
|
||||
const data: Array<{ created_at: string }> = await starsRequest.json();
|
||||
const mapDataToDate = groupBy(data, (p) =>
|
||||
dayjs(p.created_at).format('YYYY-MM-DD')
|
||||
);
|
||||
|
||||
// take all the forks from the page
|
||||
const aggForks: { [key: string]: number } = Object.values(
|
||||
mapDataToDate
|
||||
).reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[dayjs(value[0].created_at).format('YYYY-MM-DD')]: value.length,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// if we have 100 stars, we need to fetch the next page and merge the results (recursively)
|
||||
const nextOne: { [key: string]: number } =
|
||||
data.length === 100
|
||||
? await this.syncForksProcess(login, userToken, page + 1)
|
||||
: {};
|
||||
|
||||
// merge the results
|
||||
const allKeys = [
|
||||
...new Set([...Object.keys(aggForks), ...Object.keys(nextOne)]),
|
||||
];
|
||||
|
||||
return {
|
||||
...allKeys.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: (aggForks[key] || 0) + (nextOne[key] || 0),
|
||||
}),
|
||||
{} as { [key: string]: number }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async syncProcess(login: string, userToken?: string, page = 1) {
|
||||
console.log('processing stars');
|
||||
const starsRequest = await this.fetchWillFallback(
|
||||
`https://api.github.com/repos/${login}/stargazers?page=${page}&per_page=100`,
|
||||
userToken
|
||||
);
|
||||
|
||||
const data: Array<{ starred_at: string }> = await starsRequest.json();
|
||||
const mapDataToDate = groupBy(data, (p) =>
|
||||
dayjs(p.starred_at).format('YYYY-MM-DD')
|
||||
);
|
||||
|
||||
// take all the stars from the page
|
||||
const aggStars: { [key: string]: number } = Object.values(
|
||||
mapDataToDate
|
||||
).reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[dayjs(value[0].starred_at).format('YYYY-MM-DD')]: value.length,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// if we have 100 stars, we need to fetch the next page and merge the results (recursively)
|
||||
const nextOne: { [key: string]: number } =
|
||||
data.length === 100
|
||||
? await this.syncProcess(login, userToken, page + 1)
|
||||
: {};
|
||||
|
||||
// merge the results
|
||||
const allKeys = [
|
||||
...new Set([...Object.keys(aggStars), ...Object.keys(nextOne)]),
|
||||
];
|
||||
|
||||
return {
|
||||
...allKeys.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: (aggStars[key] || 0) + (nextOne[key] || 0),
|
||||
}),
|
||||
{} as { [key: string]: number }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async updateTrending(
|
||||
language: string,
|
||||
hash: string,
|
||||
arr: Array<{ name: string; position: number }>
|
||||
) {
|
||||
const currentTrending = await this._starsRepository.getTrendingByLanguage(
|
||||
language
|
||||
);
|
||||
|
||||
if (currentTrending?.hash === hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTrending) {
|
||||
const list: Array<{ name: string; position: number }> = JSON.parse(
|
||||
currentTrending.trendingList
|
||||
);
|
||||
const removedFromTrending = list.filter(
|
||||
(p) => !arr.find((a) => a.name === p.name)
|
||||
);
|
||||
const changedPosition = arr.filter((p) => {
|
||||
const current = list.find((a) => a.name === p.name);
|
||||
return current && current.position !== p.position;
|
||||
});
|
||||
if (removedFromTrending.length) {
|
||||
// let people know they are not trending anymore
|
||||
await this.inform(Inform.Removed, removedFromTrending, language);
|
||||
}
|
||||
if (changedPosition.length) {
|
||||
// let people know they changed position
|
||||
await this.inform(Inform.Changed, changedPosition, language);
|
||||
}
|
||||
}
|
||||
|
||||
const informNewPeople = arr.filter(
|
||||
(p) =>
|
||||
!currentTrending?.trendingList ||
|
||||
currentTrending?.trendingList?.indexOf(p.name) === -1
|
||||
);
|
||||
|
||||
// let people know they are trending
|
||||
await this.inform(Inform.New, informNewPeople, language);
|
||||
await this.replaceOrAddTrending(language, hash, arr);
|
||||
}
|
||||
|
||||
async inform(
|
||||
type: Inform,
|
||||
removedFromTrending: Array<{ name: string; position: number }>,
|
||||
language: string
|
||||
) {
|
||||
const names = await this._starsRepository.getGitHubsByNames(
|
||||
removedFromTrending.map((p) => p.name)
|
||||
);
|
||||
const mapDbNamesToList = names.map(
|
||||
(n) => removedFromTrending.find((p) => p.name === n.login)!
|
||||
);
|
||||
for (const person of mapDbNamesToList) {
|
||||
const getOrganizationsByGitHubLogin =
|
||||
await this._starsRepository.getOrganizationsByGitHubLogin(person.name);
|
||||
for (const org of getOrganizationsByGitHubLogin) {
|
||||
switch (type) {
|
||||
case Inform.Removed:
|
||||
return this._notificationsService.inAppNotification(
|
||||
org.organizationId,
|
||||
`${person.name} is not trending on GitHub anymore`,
|
||||
`${person.name} is not trending anymore in ${language}`,
|
||||
true
|
||||
);
|
||||
case Inform.New:
|
||||
return this._notificationsService.inAppNotification(
|
||||
org.organizationId,
|
||||
`${person.name} is trending on GitHub`,
|
||||
`${person.name} is trending in ${
|
||||
language || 'On the main feed'
|
||||
} position #${person.position}`,
|
||||
true
|
||||
);
|
||||
case Inform.Changed:
|
||||
return this._notificationsService.inAppNotification(
|
||||
org.organizationId,
|
||||
`${person.name} changed trending position on GitHub`,
|
||||
`${person.name} changed position in ${
|
||||
language || 'on the main feed to position'
|
||||
} position #${person.position}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async replaceOrAddTrending(
|
||||
language: string,
|
||||
hash: string,
|
||||
arr: Array<{ name: string; position: number }>
|
||||
) {
|
||||
return this._starsRepository.replaceOrAddTrending(language, hash, arr);
|
||||
}
|
||||
|
||||
async getStars(org: string) {
|
||||
const getGitHubs = await this.getGitHubRepositoriesByOrgId(org);
|
||||
const list = [];
|
||||
for (const gitHub of getGitHubs) {
|
||||
if (!gitHub.login) {
|
||||
continue;
|
||||
}
|
||||
const getAllByLogin = await this.getStarsByLogin(gitHub.login!);
|
||||
|
||||
const stars = getAllByLogin.filter((f) => f.stars);
|
||||
const graphSize = stars.length < 10 ? stars.length : stars.length / 10;
|
||||
|
||||
const forks = getAllByLogin.filter((f) => f.forks);
|
||||
const graphForkSize =
|
||||
forks.length < 10 ? forks.length : forks.length / 10;
|
||||
|
||||
list.push({
|
||||
login: gitHub.login,
|
||||
stars: chunk(stars, graphSize).reduce((acc, chunkedStars) => {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
totalStars: chunkedStars[chunkedStars.length - 1].totalStars,
|
||||
date: chunkedStars[chunkedStars.length - 1].date,
|
||||
},
|
||||
];
|
||||
}, [] as Array<{ totalStars: number; date: Date }>),
|
||||
forks: chunk(forks, graphForkSize).reduce((acc, chunkedForks) => {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
totalForks: chunkedForks[chunkedForks.length - 1].totalForks,
|
||||
date: chunkedForks[chunkedForks.length - 1].date,
|
||||
},
|
||||
];
|
||||
}, [] as Array<{ totalForks: number; date: Date }>),
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
async getStarsFilter(orgId: string, starsFilter: StarsListDto) {
|
||||
const getGitHubs = await this.getGitHubRepositoriesByOrgId(orgId);
|
||||
if (getGitHubs.filter((f) => f.login).length === 0) {
|
||||
return [];
|
||||
}
|
||||
return this._starsRepository.getStarsFilter(
|
||||
getGitHubs.map((p) => p.login) as string[],
|
||||
starsFilter
|
||||
);
|
||||
}
|
||||
|
||||
async addGitHub(orgId: string, code: string) {
|
||||
const { access_token } = await (
|
||||
await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: process.env.GITHUB_CLIENT_ID,
|
||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
||||
code,
|
||||
redirect_uri: `${process.env.FRONTEND_URL}/settings`,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return this._starsRepository.addGitHub(orgId, access_token);
|
||||
}
|
||||
|
||||
async getOrganizations(orgId: string, id: string) {
|
||||
const getGitHub = await this._starsRepository.getGitHubById(orgId, id);
|
||||
return (
|
||||
await fetch(`https://api.github.com/user/orgs`, {
|
||||
headers: {
|
||||
Authorization: `token ${getGitHub?.token!}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
}
|
||||
|
||||
async getRepositoriesOfOrganization(
|
||||
orgId: string,
|
||||
id: string,
|
||||
github: string
|
||||
) {
|
||||
const getGitHub = await this._starsRepository.getGitHubById(orgId, id);
|
||||
return (
|
||||
await fetch(`https://api.github.com/orgs/${github}/repos`, {
|
||||
headers: {
|
||||
Authorization: `token ${getGitHub?.token!}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
}
|
||||
|
||||
async updateGitHubLogin(orgId: string, id: string, login: string) {
|
||||
const check = await fetch(`https://github.com/${login}`);
|
||||
if (check.status === 404) {
|
||||
throw new HttpException('GitHub repository not found!', 404);
|
||||
}
|
||||
|
||||
this._workerServiceProducer
|
||||
.emit('sync_all_stars', { payload: { login } })
|
||||
.subscribe();
|
||||
return this._starsRepository.updateGitHubLogin(orgId, id, login);
|
||||
}
|
||||
|
||||
async deleteRepository(orgId: string, id: string) {
|
||||
return this._starsRepository.deleteRepository(orgId, id);
|
||||
}
|
||||
|
||||
async predictTrending(max = 500) {
|
||||
const firstDate = dayjs().subtract(1, 'day');
|
||||
return [
|
||||
firstDate.format('YYYY-MM-DDT12:00:00'),
|
||||
...[...new Array(max)].map((p, index) => {
|
||||
return firstDate.add(index, 'day').format('YYYY-MM-DDT12:00:00');
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
async predictTrendingLoop(
|
||||
trendings: Array<{ date: Date }>,
|
||||
current = 0,
|
||||
max = 500
|
||||
): Promise<Date[]> {
|
||||
const dates = trendings.map((result) => dayjs(result.date).toDate());
|
||||
const intervals = dates
|
||||
.slice(1)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map((date, i) => (date - dates[i]) / (1000 * 60 * 60 * 24));
|
||||
const nextInterval = intervals.length === 0 ? null : mean(intervals);
|
||||
const lastTrendingDate = dates[dates.length - 1];
|
||||
const nextTrendingDate = !nextInterval
|
||||
? false
|
||||
: dayjs(
|
||||
new Date(
|
||||
lastTrendingDate.getTime() + nextInterval * 24 * 60 * 60 * 1000
|
||||
)
|
||||
).toDate();
|
||||
|
||||
if (!nextTrendingDate) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
nextTrendingDate,
|
||||
...(current < max
|
||||
? await this.predictTrendingLoop(
|
||||
[...trendings, { date: nextTrendingDate }],
|
||||
current + 1,
|
||||
max
|
||||
)
|
||||
: []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -29,14 +29,6 @@ export class SubscriptionService {
|
|||
return this._subscriptionRepository.getCode(code);
|
||||
}
|
||||
|
||||
updateAccount(userId: string, account: string) {
|
||||
return this._subscriptionRepository.updateAccount(userId, account);
|
||||
}
|
||||
|
||||
getUserAccount(userId: string) {
|
||||
return this._subscriptionRepository.getUserAccount(userId);
|
||||
}
|
||||
|
||||
async deleteSubscription(customerId: string) {
|
||||
await this.modifySubscription(
|
||||
customerId,
|
||||
|
|
@ -61,14 +53,6 @@ export class SubscriptionService {
|
|||
subscriptionId
|
||||
);
|
||||
}
|
||||
|
||||
updateConnectedStatus(account: string, accountCharges: boolean) {
|
||||
return this._subscriptionRepository.updateConnectedStatus(
|
||||
account,
|
||||
accountCharges
|
||||
);
|
||||
}
|
||||
|
||||
async modifySubscription(
|
||||
customerId: string,
|
||||
totalChannels: number,
|
||||
|
|
@ -130,23 +114,6 @@ export class SubscriptionService {
|
|||
}
|
||||
|
||||
return true;
|
||||
|
||||
// if (to.faq < from.faq) {
|
||||
// await this._faqRepository.deleteFAQs(getCurrentSubscription?.organizationId, from.faq - to.faq);
|
||||
// }
|
||||
// if (to.categories < from.categories) {
|
||||
// await this._categoriesRepository.deleteCategories(getCurrentSubscription?.organizationId, from.categories - to.categories);
|
||||
// }
|
||||
// if (to.integrations < from.integrations) {
|
||||
// await this._integrationsRepository.deleteIntegrations(getCurrentSubscription?.organizationId, from.integrations - to.integrations);
|
||||
// }
|
||||
// if (to.user < from.user) {
|
||||
// await this._integrationsRepository.deleteUsers(getCurrentSubscription?.organizationId, from.user - to.user);
|
||||
// }
|
||||
// if (to.domains < from.domains) {
|
||||
// await this._settingsService.deleteDomainByOrg(getCurrentSubscription?.organizationId);
|
||||
// await this._organizationRepository.changePowered(getCurrentSubscription?.organizationId);
|
||||
// }
|
||||
}
|
||||
|
||||
async createOrUpdateSubscription(
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/pris
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Provider } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
|
||||
import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
|
||||
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
|
||||
import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
|
||||
|
||||
|
|
@ -109,17 +107,6 @@ export class UsersRepository {
|
|||
});
|
||||
}
|
||||
|
||||
changeMarketplaceActive(userId: string, active: boolean) {
|
||||
return this._user.model.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
marketplace: active,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getPersonal(userId: string) {
|
||||
const user = await this._user.model.user.findUnique({
|
||||
where: {
|
||||
|
|
@ -185,83 +172,4 @@ export class UsersRepository {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMarketplacePeople(orgId: string, userId: string, items: ItemsDto) {
|
||||
const info = {
|
||||
id: {
|
||||
not: userId,
|
||||
},
|
||||
account: {
|
||||
not: null,
|
||||
},
|
||||
connectedAccount: true,
|
||||
marketplace: true,
|
||||
items: {
|
||||
...(items.items.length
|
||||
? {
|
||||
some: {
|
||||
OR: items.items.map((key) => ({ key })),
|
||||
},
|
||||
}
|
||||
: {
|
||||
some: {
|
||||
OR: allTagsOptions.map((p) => ({ key: p.key })),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const list = await this._user.model.user.findMany({
|
||||
where: {
|
||||
...info,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
audience: true,
|
||||
picture: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
organizations: {
|
||||
select: {
|
||||
organization: {
|
||||
select: {
|
||||
Integration: {
|
||||
where: {
|
||||
disabled: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
providerIdentifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
items: {
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
skip: (items.page - 1) * 8,
|
||||
take: 8,
|
||||
});
|
||||
|
||||
const count = await this._user.model.user.count({
|
||||
where: {
|
||||
...info,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
list,
|
||||
count,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
|
||||
import { Provider } from '@prisma/client';
|
||||
import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto';
|
||||
import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
|
||||
import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
|
||||
import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
|
||||
|
|
@ -37,18 +36,6 @@ export class UsersService {
|
|||
return this._usersRepository.updatePassword(id, password);
|
||||
}
|
||||
|
||||
changeAudienceSize(userId: string, audience: number) {
|
||||
return this._usersRepository.changeAudienceSize(userId, audience);
|
||||
}
|
||||
|
||||
changeMarketplaceActive(userId: string, active: boolean) {
|
||||
return this._usersRepository.changeMarketplaceActive(userId, active);
|
||||
}
|
||||
|
||||
getMarketplacePeople(orgId: string, userId: string, body: ItemsDto) {
|
||||
return this._usersRepository.getMarketplacePeople(orgId, userId, body);
|
||||
}
|
||||
|
||||
getPersonal(userId: string) {
|
||||
return this._usersRepository.getPersonal(userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.repository';
|
||||
import { WebhooksDto } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
|
||||
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksService {
|
||||
constructor(
|
||||
private _webhooksRepository: WebhooksRepository,
|
||||
private _postsRepository: PostsRepository,
|
||||
private _workerServiceProducer: BullMqClient
|
||||
) {}
|
||||
constructor(private _webhooksRepository: WebhooksRepository) {}
|
||||
|
||||
getTotal(orgId: string) {
|
||||
return this._webhooksRepository.getTotal(orgId);
|
||||
|
|
@ -28,73 +21,4 @@ export class WebhooksService {
|
|||
deleteWebhook(orgId: string, id: string) {
|
||||
return this._webhooksRepository.deleteWebhook(orgId, id);
|
||||
}
|
||||
|
||||
async digestWebhooks(orgId: string, since: string) {
|
||||
const date = new Date().toISOString();
|
||||
await ioRedis.watch('webhook_' + orgId);
|
||||
const value = await ioRedis.get('webhook_' + orgId);
|
||||
if (value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ioRedis
|
||||
.multi()
|
||||
.set('webhook_' + orgId, date)
|
||||
.expire('webhook_' + orgId, 60)
|
||||
.exec();
|
||||
|
||||
this._workerServiceProducer.emit('webhooks', {
|
||||
id: 'digest_' + orgId,
|
||||
options: {
|
||||
delay: 60000,
|
||||
},
|
||||
payload: {
|
||||
org: orgId,
|
||||
since,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fireWebhooks(orgId: string, since: string) {
|
||||
const list = await this._postsRepository.getPostsSince(orgId, since);
|
||||
const webhooks = await this._webhooksRepository.getWebhooks(orgId);
|
||||
const sendList = [];
|
||||
for (const webhook of webhooks) {
|
||||
const toSend = [];
|
||||
if (webhook.integrations.length === 0) {
|
||||
toSend.push(...list);
|
||||
} else {
|
||||
toSend.push(
|
||||
...list.filter((post) =>
|
||||
webhook.integrations.some(
|
||||
(i) => i.integration.id === post.integration.id
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (toSend.length) {
|
||||
sendList.push({
|
||||
url: webhook.url,
|
||||
data: toSend,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
sendList.map(async (s) => {
|
||||
try {
|
||||
await fetch(s.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(s.data),
|
||||
});
|
||||
} catch (e) {
|
||||
/**empty**/
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { IsBoolean, IsIn, IsString } from 'class-validator';
|
||||
import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
|
||||
|
||||
export class AddRemoveItemDto {
|
||||
@IsString()
|
||||
@IsIn(allTagsOptions.map((p) => p.key))
|
||||
key: string;
|
||||
|
||||
@IsBoolean()
|
||||
state: boolean;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { IsNumber, Max, Min } from 'class-validator';
|
||||
|
||||
export class AudienceDto {
|
||||
@IsNumber()
|
||||
@Max(99999999)
|
||||
@Min(1)
|
||||
audience: number;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { IsBoolean } from 'class-validator';
|
||||
|
||||
export class ChangeActiveDto {
|
||||
@IsBoolean()
|
||||
active: boolean;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import {
|
||||
ArrayMinSize,
|
||||
IsNumber,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SocialMedia {
|
||||
@IsNumber()
|
||||
total: number;
|
||||
|
||||
@IsString()
|
||||
value: string;
|
||||
|
||||
@IsNumber()
|
||||
price: number;
|
||||
}
|
||||
export class CreateOfferDto {
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SocialMedia)
|
||||
socialMedia: SocialMedia[];
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { IsArray, IsIn, IsNumber, Min } from 'class-validator';
|
||||
import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list';
|
||||
|
||||
export class ItemsDto {
|
||||
@IsArray()
|
||||
@IsIn(allTagsOptions.map((p) => p.key), { each: true })
|
||||
items: string[];
|
||||
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page: number;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class NewConversationDto {
|
||||
@IsString()
|
||||
to: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(50)
|
||||
message: string;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class AddMessageDto {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
message: string;
|
||||
}
|
||||
|
|
@ -29,8 +29,9 @@ import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.pro
|
|||
import { WordpressProvider } from '@gitroom/nestjs-libraries/integrations/social/wordpress.provider';
|
||||
import { ListmonkProvider } from '@gitroom/nestjs-libraries/integrations/social/listmonk.provider';
|
||||
import { GmbProvider } from '@gitroom/nestjs-libraries/integrations/social/gmb.provider';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
|
||||
export const socialIntegrationList: SocialProvider[] = [
|
||||
export const socialIntegrationList: Array<SocialAbstract & SocialProvider> = [
|
||||
new XProvider(),
|
||||
new LinkedinProvider(),
|
||||
new LinkedinPageProvider(),
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { concurrency } from '@gitroom/helpers/utils/concurrency.service';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { ApplicationFailure } from '@temporalio/activity';
|
||||
|
||||
export class RefreshToken {
|
||||
constructor(
|
||||
public identifier: string,
|
||||
public json: string,
|
||||
public body: BodyInit,
|
||||
public message = ''
|
||||
) {}
|
||||
export class RefreshToken extends ApplicationFailure {
|
||||
constructor(identifier: string, json: string, body: BodyInit, message = '') {
|
||||
super(message, 'refresh_token', true, [
|
||||
{
|
||||
identifier,
|
||||
json,
|
||||
body,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
export class BadBody {
|
||||
constructor(
|
||||
public identifier: string,
|
||||
public json: string,
|
||||
public body: BodyInit,
|
||||
public message = ''
|
||||
) {}
|
||||
|
||||
export class BadBody extends ApplicationFailure {
|
||||
constructor(identifier: string, json: string, body: BodyInit, message = '') {
|
||||
super(message, 'bad_body', true, [
|
||||
{
|
||||
identifier,
|
||||
json,
|
||||
body,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotEnoughScopes {
|
||||
|
|
@ -51,20 +58,13 @@ export abstract class SocialAbstract {
|
|||
func: (...args: any[]) => Promise<T>,
|
||||
ignoreConcurrency?: boolean
|
||||
) {
|
||||
const value = await concurrency<any>(
|
||||
this.identifier,
|
||||
this.maxConcurrentJob,
|
||||
async () => {
|
||||
try {
|
||||
return await func();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
const handle = this.handleErrors(JSON.stringify(err));
|
||||
return { err: true, ...(handle || {}) };
|
||||
}
|
||||
},
|
||||
ignoreConcurrency
|
||||
);
|
||||
let value: any;
|
||||
try {
|
||||
value = await func();
|
||||
} catch (err) {
|
||||
const handle = this.handleErrors(JSON.stringify(err));
|
||||
value = { err: true, ...(handle || {}) };
|
||||
}
|
||||
|
||||
if (value && value?.err && value?.value) {
|
||||
throw new BadBody('', JSON.stringify({}), {} as any, value.value || '');
|
||||
|
|
@ -80,12 +80,7 @@ export abstract class SocialAbstract {
|
|||
totalRetries = 0,
|
||||
ignoreConcurrency = false
|
||||
): Promise<Response> {
|
||||
const request = await concurrency(
|
||||
this.identifier,
|
||||
this.maxConcurrentJob,
|
||||
() => fetch(url, options),
|
||||
ignoreConcurrency
|
||||
);
|
||||
const request = await fetch(url, options);
|
||||
|
||||
if (request.status === 200 || request.status === 201) {
|
||||
return request;
|
||||
|
|
@ -109,14 +104,26 @@ export abstract class SocialAbstract {
|
|||
json.includes('Rate limit')
|
||||
) {
|
||||
await timer(5000);
|
||||
return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency);
|
||||
return this.fetch(
|
||||
url,
|
||||
options,
|
||||
identifier,
|
||||
totalRetries + 1,
|
||||
ignoreConcurrency
|
||||
);
|
||||
}
|
||||
|
||||
const handleError = this.handleErrors(json || '{}');
|
||||
|
||||
if (handleError?.type === 'retry') {
|
||||
await timer(5000);
|
||||
return this.fetch(url, options, identifier, totalRetries + 1, ignoreConcurrency);
|
||||
return this.fetch(
|
||||
url,
|
||||
options,
|
||||
identifier,
|
||||
totalRetries + 1,
|
||||
ignoreConcurrency
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -239,12 +239,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
private async getAgent(integration: Integration) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
|
|
@ -261,139 +256,154 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
throw new RefreshToken('bluesky', JSON.stringify(err), {} as BodyInit);
|
||||
}
|
||||
|
||||
let loadCid = '';
|
||||
let loadUri = '';
|
||||
let replyCid = '';
|
||||
let replyUri = '';
|
||||
const cidUrl = [] as { cid: string; url: string; rev: string }[];
|
||||
for (const post of postDetails) {
|
||||
// Separate images and videos
|
||||
const imageMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
|
||||
const videoMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
|
||||
return agent;
|
||||
}
|
||||
|
||||
// Upload images
|
||||
const images = await Promise.all(
|
||||
imageMedia.map(async (p) => {
|
||||
const { buffer, width, height } = await reduceImageBySize(p.path);
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
buffer: await agent.uploadBlob(new Blob([buffer])),
|
||||
};
|
||||
})
|
||||
);
|
||||
private async uploadMediaForPost(
|
||||
agent: BskyAgent,
|
||||
post: PostDetails
|
||||
): Promise<{ embed: any; images: any[] }> {
|
||||
// Separate images and videos
|
||||
const imageMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') === -1) || [];
|
||||
const videoMedia =
|
||||
post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || [];
|
||||
|
||||
// Upload videos (only one video per post is supported by Bluesky)
|
||||
let videoEmbed: AppBskyEmbedVideo.Main | null = null;
|
||||
if (videoMedia.length > 0) {
|
||||
videoEmbed = await uploadVideo(agent, videoMedia[0].path);
|
||||
}
|
||||
|
||||
const rt = new RichText({
|
||||
text: post.message,
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// Determine embed based on media types
|
||||
let embed: any = {};
|
||||
if (videoEmbed) {
|
||||
// If there's a video, use video embed (Bluesky supports only one video per post)
|
||||
embed = videoEmbed;
|
||||
} else if (images.length > 0) {
|
||||
// If there are images but no video, use image embed
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images: images.map((p, index) => ({
|
||||
alt: imageMedia?.[index]?.alt || '',
|
||||
image: p.buffer.data.blob,
|
||||
aspectRatio: {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
},
|
||||
})),
|
||||
// Upload images
|
||||
const images = await Promise.all(
|
||||
imageMedia.map(async (p) => {
|
||||
const { buffer, width, height } = await reduceImageBySize(p.path);
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
buffer: await agent.uploadBlob(new Blob([buffer])),
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
...(Object.keys(embed).length > 0 ? { embed } : {}),
|
||||
...(loadCid
|
||||
? {
|
||||
reply: {
|
||||
root: {
|
||||
uri: replyUri,
|
||||
cid: replyCid,
|
||||
},
|
||||
parent: {
|
||||
uri: loadUri,
|
||||
cid: loadCid,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
loadCid = loadCid || cid;
|
||||
loadUri = loadUri || uri;
|
||||
replyCid = cid;
|
||||
replyUri = uri;
|
||||
|
||||
cidUrl.push({ cid, url: uri, rev: commit.rev });
|
||||
// Upload videos (only one video per post is supported by Bluesky)
|
||||
let videoEmbed: AppBskyEmbedVideo.Main | null = null;
|
||||
if (videoMedia.length > 0) {
|
||||
videoEmbed = await uploadVideo(agent, videoMedia[0].path);
|
||||
}
|
||||
|
||||
if (postDetails?.[0]?.settings?.active_thread_finisher) {
|
||||
const rt = new RichText({
|
||||
text: stripHtmlValidation(
|
||||
'normal',
|
||||
postDetails?.[0]?.settings?.thread_finisher,
|
||||
true
|
||||
),
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
await agent.post({
|
||||
text: stripHtmlValidation('normal', rt.text, true),
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
embed: {
|
||||
$type: 'app.bsky.embed.record',
|
||||
record: {
|
||||
uri: cidUrl[0].url,
|
||||
cid: cidUrl[0].cid,
|
||||
// Determine embed based on media types
|
||||
let embed: any = {};
|
||||
if (videoEmbed) {
|
||||
embed = videoEmbed;
|
||||
} else if (images.length > 0) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images: images.map((p, index) => ({
|
||||
alt: imageMedia?.[index]?.alt || '',
|
||||
image: p.buffer.data.blob,
|
||||
aspectRatio: {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
},
|
||||
},
|
||||
...(loadCid
|
||||
? {
|
||||
reply: {
|
||||
root: {
|
||||
uri: loadUri,
|
||||
cid: loadCid,
|
||||
},
|
||||
parent: {
|
||||
uri: loadUri,
|
||||
cid: loadCid,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return postDetails.map((p, index) => ({
|
||||
id: p.id,
|
||||
postId: cidUrl[index].url,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${cidUrl[index].url
|
||||
.split('/')
|
||||
.pop()}`,
|
||||
}));
|
||||
return { embed, images };
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const agent = await this.getAgent(integration);
|
||||
const [firstPost] = postDetails;
|
||||
|
||||
const { embed } = await this.uploadMediaForPost(agent, firstPost);
|
||||
|
||||
const rt = new RichText({
|
||||
text: firstPost.message,
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
...(Object.keys(embed).length > 0 ? { embed } : {}),
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: uri,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${uri.split('/').pop()}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const agent = await this.getAgent(integration);
|
||||
const [commentPost] = postDetails;
|
||||
|
||||
const { embed } = await this.uploadMediaForPost(agent, commentPost);
|
||||
|
||||
const rt = new RichText({
|
||||
text: commentPost.message,
|
||||
});
|
||||
|
||||
await rt.detectFacets(agent);
|
||||
|
||||
// Get the parent post info to get its CID
|
||||
const parentUri = lastCommentId || postId;
|
||||
|
||||
// Fetch the parent post to get its CID
|
||||
const parentThread = await agent.getPostThread({
|
||||
uri: parentUri,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const parentCid = parentThread.data.thread.post?.cid;
|
||||
// @ts-ignore
|
||||
const rootUri = parentThread.data.thread.post?.record?.reply?.root?.uri || postId;
|
||||
// @ts-ignore
|
||||
const rootCid = parentThread.data.thread.post?.record?.reply?.root?.cid || parentCid;
|
||||
|
||||
// @ts-ignore
|
||||
const { cid, uri, commit } = await agent.post({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
...(Object.keys(embed).length > 0 ? { embed } : {}),
|
||||
reply: {
|
||||
root: {
|
||||
uri: rootUri,
|
||||
cid: rootCid,
|
||||
},
|
||||
parent: {
|
||||
uri: parentUri,
|
||||
cid: parentCid,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: commentPost.id,
|
||||
postId: uri,
|
||||
status: 'completed',
|
||||
releaseURL: `https://bsky.app/profile/${id}/post/${uri.split('/').pop()}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@Plug({
|
||||
|
|
|
|||
|
|
@ -140,11 +140,76 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
let channel = postDetails[0].settings.channel;
|
||||
if (postDetails.length > 1) {
|
||||
const [firstPost] = postDetails;
|
||||
const channel = firstPost.settings.channel;
|
||||
|
||||
const form = new FormData();
|
||||
form.append(
|
||||
'payload_json',
|
||||
JSON.stringify({
|
||||
content: firstPost.message.replace(/\[\[\[(@.*?)]]]/g, (match, p1) => {
|
||||
return `<${p1}>`;
|
||||
}),
|
||||
attachments: firstPost.media?.map((p, index) => ({
|
||||
id: index,
|
||||
description: `Picture ${index}`,
|
||||
filename: p.path.split('/').pop(),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
let index = 0;
|
||||
for (const media of firstPost.media || []) {
|
||||
const loadMedia = await fetch(media.path);
|
||||
|
||||
form.append(
|
||||
`files[${index}]`,
|
||||
await loadMedia.blob(),
|
||||
media.path.split('/').pop()
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
const data = await (
|
||||
await fetch(`https://discord.com/api/channels/${channel}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
releaseURL: `https://discord.com/channels/${id}/${channel}/${data.id}`,
|
||||
postId: data.id,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const [commentPost] = postDetails;
|
||||
const channel = commentPost.settings.channel;
|
||||
|
||||
// For Discord, we create a thread from the original message for comments
|
||||
// If we don't have a thread yet, create one
|
||||
let threadChannel = channel;
|
||||
|
||||
// Create thread if this is the first comment
|
||||
if (!lastCommentId) {
|
||||
const { id: threadId } = await (
|
||||
await fetch(
|
||||
`https://discord.com/api/channels/${postDetails[0].settings.channel}/threads`,
|
||||
`https://discord.com/api/channels/${channel}/messages/${postId}/threads`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -152,64 +217,66 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: postDetails[0].message,
|
||||
name: 'Thread',
|
||||
auto_archive_duration: 1440,
|
||||
type: 11, // Public thread type
|
||||
}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
channel = threadId;
|
||||
threadChannel = threadId;
|
||||
} else {
|
||||
// Extract thread channel from the last comment's URL or use channel directly
|
||||
threadChannel = channel;
|
||||
}
|
||||
|
||||
const finalData = [];
|
||||
for (const post of postDetails) {
|
||||
const form = new FormData();
|
||||
const form = new FormData();
|
||||
form.append(
|
||||
'payload_json',
|
||||
JSON.stringify({
|
||||
content: commentPost.message.replace(/\[\[\[(@.*?)]]]/g, (match, p1) => {
|
||||
return `<${p1}>`;
|
||||
}),
|
||||
attachments: commentPost.media?.map((p, index) => ({
|
||||
id: index,
|
||||
description: `Picture ${index}`,
|
||||
filename: p.path.split('/').pop(),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
let index = 0;
|
||||
for (const media of commentPost.media || []) {
|
||||
const loadMedia = await fetch(media.path);
|
||||
|
||||
form.append(
|
||||
'payload_json',
|
||||
JSON.stringify({
|
||||
content: post.message.replace(/\[\[\[(@.*?)]]]/g, (match, p1) => {
|
||||
return `<${p1}>`;
|
||||
}),
|
||||
attachments: post.media?.map((p, index) => ({
|
||||
id: index,
|
||||
description: `Picture ${index}`,
|
||||
filename: p.path.split('/').pop(),
|
||||
})),
|
||||
})
|
||||
`files[${index}]`,
|
||||
await loadMedia.blob(),
|
||||
media.path.split('/').pop()
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
for (const media of post.media || []) {
|
||||
const loadMedia = await fetch(media.path);
|
||||
|
||||
form.append(
|
||||
`files[${index}]`,
|
||||
await loadMedia.blob(),
|
||||
media.path.split('/').pop()
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
const data = await (
|
||||
await fetch(`https://discord.com/api/channels/${channel}/messages`, {
|
||||
const data = await (
|
||||
await fetch(
|
||||
`https://discord.com/api/channels/${threadChannel}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN_ID}`,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
finalData.push({
|
||||
id: post.id,
|
||||
releaseURL: `https://discord.com/channels/${id}/${channel}/${data.id}`,
|
||||
return [
|
||||
{
|
||||
id: commentPost.id,
|
||||
releaseURL: `https://discord.com/channels/${id}/${threadChannel}/${data.id}`,
|
||||
postId: data.id,
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
return finalData;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async changeNickname(id: string, accessToken: string, name: string) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'facebook';
|
||||
|
|
@ -293,7 +294,7 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
accessToken: string,
|
||||
postDetails: PostDetails<FacebookDto>[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...comments] = postDetails;
|
||||
const [firstPost] = postDetails;
|
||||
|
||||
let finalId = '';
|
||||
let finalUrl = '';
|
||||
|
|
@ -377,36 +378,6 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
finalId = postId;
|
||||
}
|
||||
|
||||
const postsArray = [];
|
||||
let commentId = finalId;
|
||||
for (const comment of comments) {
|
||||
const data = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${commentId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(comment.media?.length
|
||||
? { attachment_url: comment.media[0].path }
|
||||
: {}),
|
||||
message: comment.message,
|
||||
}),
|
||||
},
|
||||
'add comment'
|
||||
)
|
||||
).json();
|
||||
|
||||
commentId = data.id;
|
||||
postsArray.push({
|
||||
id: comment.id,
|
||||
postId: data.id,
|
||||
releaseURL: data.permalink_url,
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
|
|
@ -414,7 +385,46 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
|
|||
releaseURL: finalUrl,
|
||||
status: 'success',
|
||||
},
|
||||
...postsArray,
|
||||
];
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<FacebookDto>[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const [commentPost] = postDetails;
|
||||
const replyToId = lastCommentId || postId;
|
||||
|
||||
const data = await (
|
||||
await this.fetch(
|
||||
`https://graph.facebook.com/v20.0/${replyToId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(commentPost.media?.length
|
||||
? { attachment_url: commentPost.media[0].path }
|
||||
: {}),
|
||||
message: commentPost.message,
|
||||
}),
|
||||
},
|
||||
'add comment'
|
||||
)
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: commentPost.id,
|
||||
postId: data.id,
|
||||
releaseURL: data.permalink_url,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ export class InstagramProvider
|
|||
integration: Integration,
|
||||
type = 'graph.facebook.com'
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...theRest] = postDetails;
|
||||
const [firstPost] = postDetails;
|
||||
console.log('in progress', id);
|
||||
const isStory = firstPost.settings.post_type === 'story';
|
||||
const medias = await Promise.all(
|
||||
|
|
@ -521,10 +521,6 @@ export class InstagramProvider
|
|||
}) || []
|
||||
);
|
||||
|
||||
const arr = [];
|
||||
|
||||
let containerIdGlobal = '';
|
||||
let linkGlobal = '';
|
||||
if (medias.length === 1) {
|
||||
const { id: mediaId } = await (
|
||||
await this.fetch(
|
||||
|
|
@ -535,22 +531,20 @@ export class InstagramProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
containerIdGlobal = mediaId;
|
||||
|
||||
const { permalink } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
linkGlobal = permalink;
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const { id: containerId, ...all3 } = await (
|
||||
await this.fetch(
|
||||
|
|
@ -589,45 +583,60 @@ export class InstagramProvider
|
|||
)
|
||||
).json();
|
||||
|
||||
containerIdGlobal = mediaId;
|
||||
|
||||
const { permalink } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: mediaId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<InstagramDto>[],
|
||||
integration: Integration,
|
||||
type = 'graph.facebook.com'
|
||||
): Promise<PostResponse[]> {
|
||||
const [commentPost] = postDetails;
|
||||
|
||||
const { id: commentId } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${postId}/comments?message=${encodeURIComponent(
|
||||
commentPost.message
|
||||
)}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
// Get the permalink from the parent post
|
||||
const { permalink } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${postId}?fields=permalink&access_token=${accessToken}`
|
||||
)
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: commentPost.id,
|
||||
postId: commentId,
|
||||
releaseURL: permalink,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
linkGlobal = permalink;
|
||||
}
|
||||
|
||||
for (const post of theRest) {
|
||||
const { id: commentId } = await (
|
||||
await this.fetch(
|
||||
`https://${type}/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent(
|
||||
post.message
|
||||
)}&access_token=${accessToken}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
arr.push({
|
||||
id: post.id,
|
||||
postId: commentId,
|
||||
releaseURL: linkGlobal,
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private setTitle(name: string) {
|
||||
|
|
|
|||
|
|
@ -165,6 +165,25 @@ export class InstagramStandaloneProvider
|
|||
);
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<InstagramDto>[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
return instagramProvider.comment(
|
||||
id,
|
||||
postId,
|
||||
lastCommentId,
|
||||
accessToken,
|
||||
postDetails,
|
||||
integration,
|
||||
'graph.instagram.com'
|
||||
);
|
||||
}
|
||||
|
||||
async analytics(id: string, accessToken: string, date: number) {
|
||||
return instagramProvider.analytics(
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -258,6 +258,25 @@ export class LinkedinPageProvider
|
|||
return super.post(id, accessToken, postDetails, integration, 'company');
|
||||
}
|
||||
|
||||
override async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
return super.comment(
|
||||
id,
|
||||
postId,
|
||||
lastCommentId,
|
||||
accessToken,
|
||||
postDetails,
|
||||
integration,
|
||||
'company'
|
||||
);
|
||||
}
|
||||
|
||||
async analytics(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
|
|
|
|||
|
|
@ -554,6 +554,17 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
isPdf
|
||||
);
|
||||
|
||||
console.log({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'LinkedIn-Version': '202501',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(postPayload),
|
||||
});
|
||||
|
||||
const response = await this.fetch('https://api.linkedin.com/rest/posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -641,11 +652,11 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
);
|
||||
}
|
||||
|
||||
const [processedFirstPost, ...restPosts] = processedPostDetails;
|
||||
const [processedFirstPost] = processedPostDetails;
|
||||
|
||||
// Process and upload media for all posts
|
||||
// Process and upload media for the first post only
|
||||
const uploadedMedia = await this.processMediaForPosts(
|
||||
processedPostDetails,
|
||||
[processedFirstPost],
|
||||
accessToken,
|
||||
id,
|
||||
type
|
||||
|
|
@ -666,25 +677,30 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
!!firstPost.settings?.post_as_images_carousel
|
||||
);
|
||||
|
||||
// Build response array starting with main post
|
||||
const responses: PostResponse[] = [
|
||||
this.createPostResponse(mainPostId, processedFirstPost.id, true),
|
||||
];
|
||||
// Return response for main post only
|
||||
return [this.createPostResponse(mainPostId, processedFirstPost.id, true)];
|
||||
}
|
||||
|
||||
// Create comment posts for remaining posts
|
||||
for (const post of restPosts) {
|
||||
const commentPostId = await this.createCommentPost(
|
||||
id,
|
||||
accessToken,
|
||||
post,
|
||||
mainPostId,
|
||||
type
|
||||
);
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<LinkedinDto>[],
|
||||
integration: Integration,
|
||||
type = 'personal' as 'company' | 'personal'
|
||||
): Promise<PostResponse[]> {
|
||||
const [commentPost] = postDetails;
|
||||
|
||||
responses.push(this.createPostResponse(commentPostId, post.id, false));
|
||||
}
|
||||
const commentPostId = await this.createCommentPost(
|
||||
id,
|
||||
accessToken,
|
||||
commentPost,
|
||||
postId,
|
||||
type
|
||||
);
|
||||
|
||||
return responses;
|
||||
return [this.createPostResponse(commentPostId, commentPost.id, false)];
|
||||
}
|
||||
|
||||
@PostPlug({
|
||||
|
|
@ -723,7 +739,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202504',
|
||||
'LinkedIn-Version': '202511',
|
||||
Authorization: `Bearer ${integration.token}`,
|
||||
},
|
||||
});
|
||||
|
|
@ -739,7 +755,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
headers: {
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json',
|
||||
'LinkedIn-Version': '202504',
|
||||
'LinkedIn-Version': '202511',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class MastodonCustomProvider extends MastodonProvider {
|
||||
override identifier = 'mastodon-custom';
|
||||
|
|
@ -81,4 +82,22 @@ export class MastodonCustomProvider extends MastodonProvider {
|
|||
postDetails
|
||||
);
|
||||
}
|
||||
|
||||
override async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
return this.dynamicComment(
|
||||
id,
|
||||
postId,
|
||||
lastCommentId,
|
||||
accessToken,
|
||||
process.env.MASTODON_URL || 'https://mastodon.social',
|
||||
postDetails
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
export class MastodonProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 5; // Mastodon instances typically have generous limits
|
||||
|
|
@ -133,47 +134,88 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider {
|
|||
url: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
let loadId = '';
|
||||
const ids = [] as string[];
|
||||
for (const getPost of postDetails) {
|
||||
const uploadFiles = await Promise.all(
|
||||
getPost?.media?.map((media) =>
|
||||
this.uploadFile(url, media.path, accessToken)
|
||||
) || []
|
||||
);
|
||||
const [firstPost] = postDetails;
|
||||
|
||||
const form = new FormData();
|
||||
form.append('status', getPost.message);
|
||||
form.append('visibility', 'public');
|
||||
if (loadId) {
|
||||
form.append('in_reply_to_id', loadId);
|
||||
const uploadFiles = await Promise.all(
|
||||
firstPost?.media?.map((media) =>
|
||||
this.uploadFile(url, media.path, accessToken)
|
||||
) || []
|
||||
);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('status', firstPost.message);
|
||||
form.append('visibility', 'public');
|
||||
if (uploadFiles.length) {
|
||||
for (const file of uploadFiles) {
|
||||
form.append('media_ids[]', file);
|
||||
}
|
||||
if (uploadFiles.length) {
|
||||
for (const file of uploadFiles) {
|
||||
form.append('media_ids[]', file);
|
||||
}
|
||||
}
|
||||
|
||||
const post = await (
|
||||
await this.fetch(`${url}/api/v1/statuses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
ids.push(post.id);
|
||||
loadId = loadId || post.id;
|
||||
}
|
||||
|
||||
return postDetails.map((p, i) => ({
|
||||
id: p.id,
|
||||
postId: ids[i],
|
||||
releaseURL: `${url}/statuses/${ids[i]}`,
|
||||
status: 'completed',
|
||||
}));
|
||||
const post = await (
|
||||
await this.fetch(`${url}/api/v1/statuses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: firstPost.id,
|
||||
postId: post.id,
|
||||
releaseURL: `${url}/statuses/${post.id}`,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async dynamicComment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
url: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const [commentPost] = postDetails;
|
||||
const replyToId = lastCommentId || postId;
|
||||
|
||||
const uploadFiles = await Promise.all(
|
||||
commentPost?.media?.map((media) =>
|
||||
this.uploadFile(url, media.path, accessToken)
|
||||
) || []
|
||||
);
|
||||
|
||||
const form = new FormData();
|
||||
form.append('status', commentPost.message);
|
||||
form.append('visibility', 'public');
|
||||
form.append('in_reply_to_id', replyToId);
|
||||
if (uploadFiles.length) {
|
||||
for (const file of uploadFiles) {
|
||||
form.append('media_ids[]', file);
|
||||
}
|
||||
}
|
||||
|
||||
const post = await (
|
||||
await this.fetch(`${url}/api/v1/statuses`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: form,
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: commentPost.id,
|
||||
postId: post.id,
|
||||
releaseURL: `${url}/statuses/${post.id}`,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async post(
|
||||
|
|
@ -188,4 +230,22 @@ export class MastodonProvider extends SocialAbstract implements SocialProvider {
|
|||
postDetails
|
||||
);
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
return this.dynamicComment(
|
||||
id,
|
||||
postId,
|
||||
lastCommentId,
|
||||
accessToken,
|
||||
process.env.MASTODON_URL || 'https://mastodon.social',
|
||||
postDetails
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue