Last active
March 5, 2025 16:09
-
-
Save ugened47/3e356e2c8c7a11bc2e1ce9280c071f6c to your computer and use it in GitHub Desktop.
Code examples from old project with Nest.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AppController } from './app.controller'; | |
import { AppService } from './app.service'; | |
import { ConfigModule } from '@nestjs/config'; | |
import { Module } from '@nestjs/common'; | |
import { TypeOrmModule } from '@nestjs/typeorm'; | |
import { BillsModule } from './bills/bills.module'; | |
import { AdminModule } from '@adminjs/nestjs'; | |
import AdminJS from 'adminjs'; | |
import { Database, Resource } from '@adminjs/typeorm'; | |
import { ServeStaticModule } from '@nestjs/serve-static'; | |
import { join } from 'path'; | |
import { LoggerModule, PinoLogger } from 'nestjs-pino'; | |
import { PinoTypeormLogger } from './common/utils/pino.typeorm.logger'; | |
import { ProposalsModule } from './proposals/proposals.module'; | |
import appConfig from './config/app.config'; | |
import { UsersModule } from './users/users.module'; | |
import { AuthModule } from './auth/auth.module'; | |
import { GroupsModule } from './groups/groups.module'; | |
import { RolesModule } from './roles/roles.module'; | |
import { getConnectionOptions } from 'typeorm'; | |
import { adminResources } from './admin.options'; | |
import { TeamsModule } from './teams/teams.module'; | |
import { LikesModule } from './likes/likes.module'; | |
import { InvitationsModule } from './invitations/invitations.module'; | |
import { ScheduleModule } from '@nestjs/schedule'; | |
import { User } from './users/entities/user.entity'; | |
import * as bcrypt from 'bcrypt'; | |
import { Role as RoleEnum } from 'src/auth/role.enum'; | |
import { MailCronService } from './cron/mail.cron'; | |
import { newBillMailing } from './jobs/newBillMailing.job'; | |
import { SubscriptionsModule } from './subscriptions/subscriptions.module'; | |
AdminJS.registerAdapter({ Database, Resource }); | |
@Module({ | |
imports: [ | |
ScheduleModule.forRoot(), | |
ConfigModule.forRoot({ | |
envFilePath: `.env.${process.env.NODE_ENV}`, | |
ignoreEnvFile: process.env.NODE_ENV === 'production', | |
load: [appConfig], | |
isGlobal: true, | |
}), | |
LoggerModule.forRoot({ | |
pinoHttp: { | |
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info', | |
prettyPrint: process.env.NODE_ENV !== 'production', | |
}, | |
}), | |
ServeStaticModule.forRoot({ | |
rootPath: join(__dirname, '..', 'public'), | |
exclude: ['/api*'], | |
}), | |
TypeOrmModule.forRootAsync({ | |
useFactory: async (logger: PinoLogger) => | |
Object.assign(await getConnectionOptions(), { | |
autoLoadEntities: true, | |
logging: true, | |
logger: new PinoTypeormLogger(logger), | |
}), | |
inject: [PinoLogger], | |
}), | |
AdminModule.createAdmin({ | |
adminJsOptions: { | |
rootPath: '/admin', | |
branding: { | |
companyName: 'OpenSaeima', | |
softwareBrothers: false, | |
logo: false, | |
}, | |
resources: adminResources, | |
}, | |
auth: { | |
authenticate: async (email, password) => { | |
const adminResolver = adminResources.find( | |
(el) => el.resource === User, | |
); | |
if (adminResolver.resource === User) { | |
const admin = await adminResolver.resource.findOne({ | |
email: email, | |
}); | |
let adminRole = null; | |
let isPasswordMatching = false; | |
if (admin) { | |
adminRole = admin?.roles.find( | |
(role) => role.value === RoleEnum.SystemAdmin, | |
); | |
isPasswordMatching = await bcrypt.compare( | |
password, | |
admin.password, | |
); | |
} | |
if ( | |
adminRole === null || | |
adminRole === undefined || | |
isPasswordMatching === false | |
) { | |
return Promise.resolve(null); | |
} | |
return Promise.resolve({ email: email }); | |
} | |
return Promise.resolve(null); | |
}, | |
cookieName: 'sessionCookie', | |
cookiePassword: String(process.env.COOKIE_SECRET), | |
}, | |
}), | |
AuthModule, | |
BillsModule, | |
ProposalsModule, | |
UsersModule, | |
GroupsModule, | |
RolesModule, | |
TeamsModule, | |
LikesModule, | |
InvitationsModule, | |
SubscriptionsModule, | |
], | |
controllers: [AppController], | |
providers: [AppService, MailCronService, newBillMailing], | |
}) | |
export class AppModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; | |
import { ApiProperty } from '@nestjs/swagger'; | |
export class CreateProposalDto { | |
@ApiProperty() | |
@IsString() | |
@IsUUID() | |
readonly uuid: string; | |
@ApiProperty() | |
@IsOptional() | |
@IsString() | |
readonly content?: string; | |
@ApiProperty() | |
@IsString() | |
readonly refNodeJson: string; | |
@ApiProperty() | |
@IsOptional() | |
@IsString() | |
readonly contentJson?: string; | |
@ApiProperty() | |
@IsNumber() | |
readonly documentId: number; | |
@ApiProperty() | |
@IsNumber() | |
readonly teamId: number; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; | |
import { Alert, Platform } from 'react-native'; | |
import { MaterialCommunityIcons } from '@expo/vector-icons'; | |
import * as DocumentPicker from 'expo-document-picker'; | |
import convertToDraftJSON from 'plain-text-to-draftjs'; | |
import { replaceMentionValues } from 'react-native-controlled-mentions'; | |
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; | |
import { EmojiType } from 'rn-emoji-keyboard/lib/typescript/src/types'; | |
import { SearchUserResult } from '@authencity/api/src'; | |
import { | |
GradientButton, | |
Paragraph, | |
styled, | |
XStack, | |
YStack, | |
Spinner, | |
CustomTabs, | |
} from '@authencity/ui'; | |
import { AttachmentList } from 'app/components/Attachments'; | |
import { AttachmentsActionBar } from 'app/components/AttachmentsActionBar'; | |
import Container from 'app/components/Container'; | |
import { AuthCurrency, CreatePostFileTypeEnum } from 'app/constants/app'; | |
import useAuth from 'app/context/auth'; | |
import { HomeStackScreenProps } from 'app/types/navigation'; | |
import { ISendPublication } from 'app/types/user'; | |
import NewThreadItem from './NewThreadItem'; | |
import useCreatePost from './hooks/useCreatePost'; | |
const TextLengthBedge = styled(XStack, { | |
name: 'TextLengthBedge', | |
bg: '$background', | |
alignSelf: 'flex-start', | |
px: '$2', | |
borderRadius: '$true', | |
mx: 12, | |
}); | |
const ATTACHMENT_MIME_TYPE = { | |
[CreatePostFileTypeEnum.image]: ['image/jpeg', 'image/png'], | |
[CreatePostFileTypeEnum.image360]: ['image/jpeg', 'image/png'], | |
[CreatePostFileTypeEnum.video]: ['video/mp4', 'video/webm'], | |
[CreatePostFileTypeEnum.gif]: ['image/gif'], | |
[CreatePostFileTypeEnum.audio]: ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg'], | |
[CreatePostFileTypeEnum.image3D]: ['model/gltf-binary', 'model/gltf+json'], | |
}; | |
interface IAttachment { | |
uri: string; | |
name: string; | |
type: string; | |
attachmentType: CreatePostFileTypeEnum; | |
} | |
interface IThreadItem extends Omit<ISendPublication, 'IpfsFiles'> { | |
IpfsFiles: IAttachment[]; | |
Mentions: SearchUserResult[]; | |
Count: number; | |
} | |
type OnTextChangeArgs = { | |
value: string; | |
mentions: SearchUserResult[]; | |
}; | |
enum PublicationType { | |
blockchain = 'Publication', | |
encrypted = 'Encrypted_publication', | |
superpost = 'Superpost', | |
blogpost = 'Blogpost', | |
} | |
const INITIAL_THREAD_ITEMS: IThreadItem[] = [ | |
{ | |
Id: '0', | |
EditorData: { | |
blocks: [], | |
entityMap: {}, | |
}, | |
IpfsFiles: [], | |
Text: '', | |
Mentions: [], | |
Count: 0, | |
}, | |
]; | |
export default function NewPublicationScreen({ | |
navigation, | |
}: HomeStackScreenProps<'NewPublicationModal'>) { | |
const [threadItems, _setThreadItems] = useState<IThreadItem[]>(INITIAL_THREAD_ITEMS); | |
const threadItemsRef = useRef(threadItems); | |
const setThreadItems = (value: IThreadItem[]) => { | |
threadItemsRef.current = value; | |
_setThreadItems(value); | |
}; | |
const [activeThreadItemId, setActiveThreadItemId] = useState<string>('0'); | |
const [publicationType, setPublicationType] = useState<PublicationType>( | |
PublicationType.blockchain | |
); | |
const { handleMint, isMinting, publication } = useCreatePost(publicationType); | |
const [currency, setCurrency] = useState<AuthCurrency>(AuthCurrency.ethereum); | |
const { currentUser } = useAuth(); | |
const submitDisabled = useMemo(() => { | |
return threadItems.some((item) => { | |
return !item?.Text && item?.IpfsFiles?.length === 0; | |
}); | |
}, [threadItems]); | |
useEffect(() => { | |
// Use `setOptions` to update the button that we previously specified | |
// Now the button includes an `onPress` handler to update the count | |
navigation.setOptions({ | |
headerRight: () => ( | |
<GradientButton | |
disabled={submitDisabled || isMinting} | |
w={74} | |
h={32} | |
fontSize="$2" | |
onPress={handleMintPress} | |
> | |
{isMinting ? <Spinner color="$white" /> : 'Mint'} | |
</GradientButton> | |
), | |
}); | |
}, [navigation, threadItems, currency, isMinting, submitDisabled]); | |
const hasUnsavedChanges = threadItems.some( | |
(item) => item.Text!.length > 0 || item.IpfsFiles.length > 0 | |
); | |
useEffect( | |
() => | |
navigation.addListener('beforeRemove', (e) => { | |
if (!hasUnsavedChanges || !!publication) { | |
// If we don't have unsaved changes, then we don't need to do anything | |
return; | |
} | |
// Prevent default behavior of leaving the screen | |
e.preventDefault(); | |
// Prompt the user before leaving the screen | |
Alert.alert( | |
'Discard changes?', | |
'You have unsaved changes. Are you sure to discard them and leave the screen?', | |
[ | |
{ text: "Don't leave", style: 'cancel', onPress: () => {} }, | |
{ | |
text: 'Discard', | |
style: 'destructive', | |
// If the user confirmed, then we dispatch the action we blocked earlier | |
// This will continue the action that had triggered the removal of the screen | |
onPress: () => navigation.dispatch(e.data.action), | |
}, | |
] | |
); | |
}), | |
[navigation, hasUnsavedChanges, publication] | |
); | |
const addThreadItem = () => { | |
const newThreadItem = { | |
Id: threadItems.length.toString(), | |
EditorData: { | |
blocks: [], | |
entityMap: {}, | |
}, | |
IpfsFiles: [], | |
Text: '', | |
Mentions: [], | |
Count: 0, | |
}; | |
setThreadItems([...threadItems, newThreadItem]); | |
}; | |
const removeThreadItem = (id: string) => { | |
const newThreadItems = threadItems.filter((item) => item.Id !== id); | |
if (newThreadItems.length > 0) { | |
setActiveThreadItemId(newThreadItems?.[newThreadItems?.length - 1]?.Id as string); | |
} | |
setThreadItems(newThreadItems); | |
}; | |
const selectFile = async (attachmentType: CreatePostFileTypeEnum) => { | |
try { | |
const result = await DocumentPicker.getDocumentAsync({ | |
copyToCacheDirectory: false, | |
type: ATTACHMENT_MIME_TYPE[attachmentType], | |
}); | |
result.assets?.forEach((asset) => { | |
const file = { | |
name: asset.name, | |
type: asset.mimeType, | |
uri: Platform.OS === 'ios' ? asset.uri.replace('file://', '') : asset.uri, | |
attachmentType, | |
} as IAttachment; | |
handleThreadItemAddFile(activeThreadItemId)(file); | |
}); | |
} catch (err) { | |
console.warn(err); | |
return false; | |
} | |
}; | |
const handleThreadItemTextChange = | |
(Id: string) => | |
({ value, mentions }: OnTextChangeArgs) => { | |
const newThreadItems = threadItemsRef?.current?.map((item) => { | |
if (item.Id === Id) { | |
const plainText = replaceMentionValues(value, ({ name }) => `@${name}`); | |
const draftEditorState = convertToDraftJSON(plainText, mentions, 'NickName'); | |
return { | |
...item, | |
EditorData: draftEditorState, | |
Text: value, | |
Count: plainText.length, | |
Mentions: mentions, | |
}; | |
} | |
return item; | |
}); | |
setThreadItems(newThreadItems); | |
}; | |
const handleThreadItemAddFile = (Id: string) => (file: IAttachment) => { | |
const newThreadItems = threadItemsRef?.current?.map((item) => { | |
if (item.Id === Id) { | |
return { | |
...item, | |
IpfsFiles: [...item.IpfsFiles, file], | |
}; | |
} | |
return item; | |
}); | |
setThreadItems(newThreadItems); | |
}; | |
const handleThreadItemRemoveFile = (Id: string) => (file: IAttachment) => { | |
const newThreadItems = threadItemsRef?.current?.map((item) => { | |
if (item.Id === Id) { | |
return { | |
...item, | |
IpfsFiles: item.IpfsFiles.filter((f) => f.uri !== file.uri), | |
}; | |
} | |
return item; | |
}); | |
setThreadItems(newThreadItems); | |
}; | |
const showThreadLine = (index: number) => { | |
return index !== threadItems.length - 1; | |
}; | |
const showRemoveButton = (index: number) => { | |
return index === threadItems.length - 1 && index !== 0; | |
}; | |
const tabs = [ | |
{ | |
value: PublicationType.blockchain, | |
label: 'Blockchain', | |
}, | |
{ | |
value: PublicationType.encrypted, | |
label: 'Encrypted', | |
}, | |
]; | |
const handlePublicationTypeChange = (value: PublicationType) => { | |
setPublicationType(value); | |
_setThreadItems(INITIAL_THREAD_ITEMS); | |
}; | |
const handleAddEmoji = (emoji: EmojiType) => { | |
const newThreadItems = threadItemsRef?.current?.map((item) => { | |
if (item.Id === activeThreadItemId) { | |
const newText = (item.Text ?? '') + emoji.emoji; | |
const plainText = replaceMentionValues(newText, ({ name }) => `@${name}`); | |
const draftEditorState = convertToDraftJSON(plainText, item.Mentions, 'NickName'); | |
return { | |
...item, | |
Text: (item.Text ?? '') + emoji.emoji, | |
EditorData: draftEditorState, | |
Count: plainText.length, | |
}; | |
} | |
return item; | |
}); | |
setThreadItems(newThreadItems); | |
}; | |
const handleMintPress = async () => { | |
handleMint(threadItems, currency); | |
}; | |
return ( | |
<Container> | |
<KeyboardAwareScrollView | |
contentContainerStyle={{ flexGrow: 1, paddingTop: 90, paddingHorizontal: 16 }} | |
> | |
<XStack jc="space-between" space="$6" ai="center" mt="$3"> | |
<XStack ml={8}> | |
<GradientButton | |
disabled={submitDisabled} | |
unstyled | |
w={18} | |
h={18} | |
borderRadius={100_000} | |
pressStyle={{ | |
opacity: 0.5, | |
}} | |
animation="quick" | |
ai="center" | |
jc="center" | |
// circular | |
// size={18} | |
bw={0} | |
onPress={() => addThreadItem()} | |
> | |
<MaterialCommunityIcons name="plus-thick" size={8} color="#fff" /> | |
</GradientButton> | |
</XStack> | |
<YStack f={1}> | |
<CustomTabs tabs={tabs} onTabPress={handlePublicationTypeChange} /> | |
</YStack> | |
</XStack> | |
<YStack mt={12}> | |
{threadItems.map((threadItem, i) => ( | |
<NewThreadItem key={threadItem.Id} isActive={activeThreadItemId === threadItem.Id}> | |
<NewThreadItem.Avatar | |
src={currentUser?.ImageLink!} | |
showThreadLine={showThreadLine(i)} | |
showRemoveButton={showRemoveButton(i)} | |
onRemovePress={() => removeThreadItem(threadItem.Id)} | |
/> | |
<NewThreadItem.Content f={1}> | |
<TextLengthBedge> | |
<Paragraph size="$2" color="$color" fontWeight="700"> | |
{threadItem.Count}/5000 | |
</Paragraph> | |
</TextLengthBedge> | |
<NewThreadItem.Editor | |
value={threadItem.Text ?? ''} | |
onFocus={() => setActiveThreadItemId(threadItem.Id)} | |
onTextChange={handleThreadItemTextChange(threadItem.Id)} | |
/> | |
<AttachmentList | |
// onPress={() => setActiveThreadItemId(threadItem.Id)} | |
files={threadItem.IpfsFiles} | |
onRemoveFile={handleThreadItemRemoveFile(threadItem.Id)} | |
itemSize={150} | |
/> | |
</NewThreadItem.Content> | |
</NewThreadItem> | |
))} | |
</YStack> | |
<YStack py={20} mt="auto"> | |
{publicationType === PublicationType.blockchain && ( | |
<AttachmentsActionBar | |
mb={12} | |
onSelectFile={selectFile} | |
currency={currency} | |
onCurrencyChange={setCurrency} | |
onAddEmoji={handleAddEmoji} | |
/> | |
)} | |
</YStack> | |
{/* <Modal isVisible={isMinting}> | |
<YStack f={1} ai="center" jc="center"> | |
<Spinner color="$color" size="large" /> | |
</YStack> | |
</Modal> */} | |
</KeyboardAwareScrollView> | |
</Container> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BaseEntity, | |
Column, | |
CreateDateColumn, | |
Entity, | |
ManyToOne, | |
OneToMany, | |
PrimaryGeneratedColumn, | |
RelationId, | |
UpdateDateColumn, | |
} from 'typeorm'; | |
import Document from '../../bills/entities/document.entity'; | |
import { Team } from '../../teams/entities/team.entity'; | |
import { Like } from 'src/likes/entities/like.entity'; | |
@Entity() | |
export class Proposal extends BaseEntity { | |
@PrimaryGeneratedColumn() | |
id: number; | |
@Column({ type: 'uuid' }) | |
uuid: string; | |
@Column('jsonb', { nullable: true }) | |
refNodeJson: string; | |
@Column('text', { nullable: true }) | |
content: string; | |
@Column('jsonb', { nullable: true }) | |
contentJson: string; | |
@CreateDateColumn() | |
createdAt: Date; | |
@UpdateDateColumn() | |
updatedAt: Date; | |
@ManyToOne(() => Document, (document) => document.proposals) | |
document?: Document; | |
@ManyToOne(() => Team, (team) => team.proposals, { | |
onDelete: 'CASCADE', | |
}) | |
team?: Team; | |
@RelationId((proposal: Proposal) => proposal.document) | |
documentId?: number; | |
@RelationId((proposal: Proposal) => proposal.team) | |
teamId: number; | |
@OneToMany(() => Like, (like) => like.proposal, { | |
eager: true, | |
cascade: true, | |
}) | |
likes: Like[]; | |
@Column('boolean', { nullable: true }) | |
isPublished: boolean; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Controller, | |
Get, | |
Post, | |
Body, | |
Patch, | |
Param, | |
Delete, | |
UseGuards, | |
} from '@nestjs/common'; | |
import { ProposalsService } from './proposals.service'; | |
import { CreateProposalDto } from './dto/create-proposal.dto'; | |
import { UpdateProposalDto } from './dto/update-proposal.dto'; | |
import { GetProposalsDto } from './dto/get-proposals.dto'; | |
import JwtGuard from 'src/auth/jwt.guard'; | |
import { Roles } from 'src/auth/roles-auth.decorator'; | |
import { Role as RolesEnum } from 'src/auth/role.enum'; | |
import { RolesGuard } from 'src/auth/roles.guard'; | |
@Controller() | |
export class ProposalsController { | |
constructor(private readonly proposalsService: ProposalsService) {} | |
@Post('proposals') | |
create(@Body() createProposalDto: CreateProposalDto) { | |
return this.proposalsService.create(createProposalDto); | |
} | |
@Get('proposals/bills/:billId') | |
findAllForBill(@Param('billId') billId: string) { | |
return this.proposalsService.findByBill(billId); | |
} | |
@Get('proposals/documents/:documentId') | |
findAllForDocument(@Param('documentId') documentId: number) { | |
const prop = this.proposalsService.findByDocument(documentId); | |
return prop; | |
} | |
@Post('proposals/documents') | |
async findByDocumentsAndTeam(@Body() getProposalsDto: GetProposalsDto) { | |
const prop = await this.proposalsService.findByDocumentsAndTeam( | |
getProposalsDto, | |
); | |
return prop; | |
} | |
@Post('proposals/documents') | |
findAllForDocuments(@Body() documentIds: number[]) { | |
const props = this.proposalsService.findByDocuments(documentIds); | |
return props; | |
} | |
@Get('proposals/:uuid') | |
findOne(@Param('uuid') uuid: string) { | |
return this.proposalsService.findOne(uuid); | |
} | |
@UseGuards(JwtGuard) | |
@Patch('proposals/:id') | |
update( | |
@Param('id') id: string, | |
@Body() updateProposalDto: UpdateProposalDto, | |
) { | |
return this.proposalsService.update(+id, updateProposalDto); | |
} | |
@UseGuards(JwtGuard, RolesGuard) | |
@Patch('admin/proposals/:id') | |
@Roles(RolesEnum.User, RolesEnum.Moderator, RolesEnum.SystemAdmin) | |
updateComment( | |
@Param('id') id: string, | |
@Body() updateProposalDto: UpdateProposalDto, | |
) { | |
return this.proposalsService.update(+id, updateProposalDto); | |
} | |
@Patch('proposals/:id/publish') | |
publish(@Param('id') id: number, @Body() body: any) { | |
return this.proposalsService.publish(id, body.userId); | |
} | |
@UseGuards(JwtGuard, RolesGuard) | |
@Roles(RolesEnum.GroupAdmin, RolesEnum.Moderator, RolesEnum.SystemAdmin) | |
@Delete('proposals/:id') | |
remove(@Param('id') id: string) { | |
return this.proposalsService.remove(+id); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Module } from '@nestjs/common'; | |
import { ProposalsService } from './proposals.service'; | |
import { ProposalsController } from './proposals.controller'; | |
import { TypeOrmModule } from '@nestjs/typeorm'; | |
import { Proposal } from './entities/proposal.entity'; | |
import { BillsModule } from 'src/bills/bills.module'; | |
import { UsersModule } from 'src/users/users.module'; | |
@Module({ | |
imports: [TypeOrmModule.forFeature([Proposal]), BillsModule, UsersModule], | |
controllers: [ProposalsController], | |
providers: [ProposalsService], | |
exports: [TypeOrmModule, ProposalsService], | |
}) | |
export class ProposalsModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable, NotFoundException } from '@nestjs/common'; | |
import { InjectRepository } from '@nestjs/typeorm'; | |
import { BillsService } from 'src/bills/bills.service'; | |
import { UsersService } from 'src/users/users.service'; | |
import Bill from 'src/bills/entities/bill.entity'; | |
import Document from 'src/bills/entities/document.entity'; | |
import { In, Repository } from 'typeorm'; | |
import { CreateProposalDto } from './dto/create-proposal.dto'; | |
import { UpdateProposalDto } from './dto/update-proposal.dto'; | |
import { GetProposalsDto } from './dto/get-proposals.dto'; | |
import { Proposal } from './entities/proposal.entity'; | |
@Injectable() | |
export class ProposalsService { | |
constructor( | |
@InjectRepository(Proposal) | |
private readonly proposalRepository: Repository<Proposal>, | |
private readonly billsService: BillsService, | |
private readonly usersService: UsersService, | |
) {} | |
async create(createProposalDto: CreateProposalDto) { | |
const bill = await this.billsService.findOneByDocumentId( | |
createProposalDto.documentId, | |
); | |
if (bill.deadline && bill.deadline <= new Date()) | |
throw new NotFoundException( | |
'The deadline for adding comment is expired.', | |
); | |
const newProposal: any = { | |
uuid: createProposalDto.uuid, | |
refNodeJson: createProposalDto.refNodeJson, | |
content: createProposalDto.content, | |
contentJson: createProposalDto.contentJson, | |
createdAt: new Date(), | |
document: { | |
id: createProposalDto.documentId, | |
}, | |
team: { | |
id: createProposalDto.teamId, | |
}, | |
}; | |
const proposal = this.proposalRepository.create(newProposal); | |
await this.proposalRepository.save(proposal); | |
return proposal; | |
} | |
async findByBill(billId: string) { | |
const bill = await this.billsService.findOne(billId); | |
const documentIds = bill.documents?.length | |
? bill.documents.map((a) => a.id) | |
: []; | |
const data = await this.proposalRepository.find({ | |
where: { | |
document: In(documentIds), | |
isPublished: true, | |
}, | |
relations: ['team'], | |
}); | |
const uuids = []; | |
bill.documents.forEach((document) => { | |
if (!document.contentJson) return; | |
const json = document.contentJson; | |
JSON.parse(json, function (key, value) { | |
if (key === 'data-uuid' && !!value) uuids.push(value); | |
return value; | |
}); | |
}); | |
const result = { | |
billName: bill.name, | |
data: [], | |
}; | |
const sortedProposals = data.sort( | |
(a, b) => uuids.indexOf(a.uuid) - uuids.indexOf(b.uuid), | |
); | |
sortedProposals.forEach((a) => { | |
const refNode = JSON.parse(a.refNodeJson); | |
if (refNode.type === 'table' && refNode.content?.length) { | |
let text = ''; | |
let element = | |
'<table style="border: 1px solid black; width: 100%; border-collapse: collapse;">'; | |
refNode.content.forEach((tContent) => { | |
if (tContent.type === 'tableRow' && tContent.content?.length) { | |
element += '<tr style="border: 1px solid black;">'; | |
tContent.content.forEach((rContent) => { | |
if (rContent.type === 'tableCell' && rContent.content?.length) { | |
element += '<td style="border: 1px solid black; padding: 5px">'; | |
rContent.content.forEach((cContent) => { | |
if ( | |
cContent.type === 'paragraph' && | |
cContent.content?.length | |
) { | |
cContent.content.forEach((pContent) => { | |
if (pContent.type === 'text' && pContent.text) { | |
element += `<p>${pContent.text}</p>`; | |
text += pContent.text + ' '; | |
} | |
}); | |
} | |
}); | |
element += '</td>'; | |
} | |
}); | |
element += '</tr>'; | |
} | |
}); | |
element += '<table>'; | |
result.data.push({ | |
text: text, | |
element: element, | |
content: a.content, | |
publishedTime: a.createdAt, | |
team: a.team ? a.team : '-', | |
teamId: a.team ? a.team.id : '-', | |
}); | |
} else { | |
const firstContent = refNode.content[0]; | |
result.data.push({ | |
text: firstContent.text, | |
element: null, | |
content: a.content, | |
publishedTime: a.createdAt, | |
team: a.team ? a.team : '-', | |
teamId: a.team ? a.team.id : '-', | |
}); | |
} | |
}); | |
return result; | |
} | |
findByDocument(documentId: number) { | |
return this.proposalRepository.find({ where: { document: documentId } }); | |
} | |
async findByDocumentsAndTeam(getProposalsDto: GetProposalsDto) { | |
const proposals = await this.proposalRepository.find({ | |
where: [ | |
{ | |
document: In(getProposalsDto.documentIds), | |
team: getProposalsDto.teamId, | |
}, | |
{ document: In(getProposalsDto.documentIds), isPublished: true }, | |
], | |
relations: ['team'], | |
}); | |
return proposals.map((proposal) => { | |
return { | |
...proposal, | |
teamName: proposal.team?.name, | |
}; | |
}); | |
} | |
findByDocuments(documentIds: number[]) { | |
return this.proposalRepository.find({ | |
where: { document: In(documentIds) }, | |
}); | |
} | |
findAll() { | |
return `This action returns all proposals`; | |
} | |
async findOne(uuid: string) { | |
const proposal = await this.proposalRepository.findOne({ uuid }); | |
if (!proposal) { | |
throw new NotFoundException(`Can't find proposal with id ${uuid}`); | |
} | |
return proposal; | |
} | |
async update(id: number, updateProposalDto: UpdateProposalDto) { | |
const proposal = await this.proposalRepository.preload({ | |
id: +id, | |
...updateProposalDto, | |
}); | |
if (!proposal) { | |
throw new NotFoundException(`Can't find proposal with id ${id}`); | |
} | |
await this.proposalRepository.save(proposal); | |
const updatedProposal = await this.proposalRepository.findOne({ | |
where: { id }, | |
relations: ['team'], | |
}); | |
return { | |
...updatedProposal, | |
teamName: updatedProposal.team?.name, | |
}; | |
} | |
async publish(id: number, userId: number) { | |
const user = await this.usersService.findOne(userId); | |
if (user) { | |
const proposal = await this.proposalRepository.preload({ | |
id: id, | |
...{ | |
isPublished: true, | |
}, | |
}); | |
if (!proposal) { | |
throw new NotFoundException(`Can't find proposal with id ${id}`); | |
} | |
const _proposal = await this.proposalRepository.findOne(id, { | |
relations: ['team'], | |
}); | |
if ( | |
_proposal && | |
_proposal.team && | |
user.teams.some((a) => a.name === _proposal.team.name) | |
) { | |
return this.proposalRepository.save(proposal); | |
} | |
} | |
return { | |
message: "You can't publish this proposal", | |
}; | |
} | |
async remove(id: number) { | |
const proposal = await this.proposalRepository.preload({ | |
id: +id, | |
}); | |
if (!proposal) { | |
throw new NotFoundException(`Can't find proposal with id ${id}`); | |
} | |
return this.proposalRepository.remove(proposal); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Component from some ts project with react-native | |
import React, { Suspense, memo, useCallback, useMemo } from 'react'; | |
import { StyleProp, TextStyle, TouchableOpacity } from 'react-native'; | |
import { Linking, Platform } from 'react-native'; | |
import { BlurView as RNBlurView } from '@react-native-community/blur'; | |
import dayjs from 'dayjs'; | |
import { RawDraftContentState } from 'draft-js'; | |
import { BlurView } from 'expo-blur'; | |
import Svg, { | |
SvgProps, | |
G, | |
Path, | |
Defs, | |
LinearGradient, | |
Stop, | |
ClipPath, | |
Circle, | |
} from 'react-native-svg'; | |
import { useRouter } from 'solito/router'; | |
import { PublicationDTO } from '@authencity/api/src'; | |
import { | |
Button, | |
H1, | |
NetworkIcon, | |
Paragraph, | |
UserAvatar, | |
XStack, | |
XStackProps, | |
YStack, | |
YStackProps, | |
styled, | |
useTheme, | |
useToastController, | |
} from '@authencity/ui'; | |
import { CreatePostFileTypeEnum } from 'app/constants/app'; | |
import useAuth from 'app/context/auth'; | |
import useBackPost from 'app/context/post-backing'; | |
import useColorMode from 'app/hooks/useColorMode'; | |
import { PublicationDecryptProvider, usePublicationDecrypt } from 'app/hooks/useDecryptPublication'; | |
import usePublicationActions from 'app/hooks/usePublicationActions'; | |
import { getOpenSeaLink, getPostTextFromEditorState } from 'app/utils/blockchain'; | |
import { getTimeAgo } from 'app/utils/dates'; | |
import { getTransactionLink } from 'app/utils/networkType'; | |
import Config from '../../../../apps/expo/config'; | |
import { ErrorHandler } from '../ErrorHandler/ErrorHandler'; | |
import ExternalLink from '../ExternalLink'; | |
import { | |
MentionHashtagTextViewFull, | |
MentionHashtagTextViewSmall, | |
} from '../MentionHashtagTextView/MentionHashtagTextView'; | |
import PostingMedia from '../PostingMedia'; | |
import { | |
IconBookmark, | |
IconComment, | |
IconCopy, | |
IconLike, | |
IconRetweet, | |
IconView, | |
IconOpensea, | |
} from './PublicationIcons'; | |
import { PublicationLink } from './PublicationLink'; | |
import PublicationSkeleton from './PublicationSkeleton'; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationHeader | |
* -----------------------------------------------------------------------------------------------*/ | |
const Nick = styled(Paragraph, { | |
name: 'Nick', | |
fontSize: '$2', | |
fontWeight: '700', | |
color: '$secondary', | |
}); | |
const Name = styled(H1, { | |
name: 'Name', | |
fontSize: '$true', | |
lineHeight: 14, | |
m: 0, | |
}); | |
const LockIconClosed = memo((props: SvgProps) => ( | |
<Svg width={24} height={24} fill="none" {...props}> | |
<Path | |
fill="currentColor" | |
fillRule="evenodd" | |
d="M14.75 7.75v1.7c0 .427.304.85.73.85h.108c.336 0 .631-.237.653-.572.006-.092.009-.184.009-.278v-1.7a4.25 4.25 0 0 0-8.5 0v1.7c0 .094.003.186.009.278.022.335.317.572.653.572h.108c.426 0 .73-.423.73-.85v-1.7a2.75 2.75 0 1 1 5.5 0Z" | |
clipRule="evenodd" | |
/> | |
<Path | |
stroke="currentColor" | |
strokeWidth={1.5} | |
d="M5.95 12.213c0-1.112.901-2.013 2.013-2.013h8.075c1.111 0 2.012.901 2.012 2.013V13.7a6.05 6.05 0 0 1-12.1 0v-1.487Z" | |
/> | |
<Circle cx={12} cy={13.7} r={1.375} stroke="currentColor" strokeWidth={1.5} /> | |
<Path stroke="currentColor" strokeLinecap="round" strokeWidth={1.5} d="M12 15.4v1.7" /> | |
</Svg> | |
)); | |
const LockIconOpen = memo((props: SvgProps) => ( | |
<Svg width={24} height={24} fill="none" {...props}> | |
<Path | |
fill="currentColor" | |
fillRule="evenodd" | |
d="M14.81 10a.147.147 0 0 0-.14.113.15.15 0 0 0 .14.187h1.228a.151.151 0 0 0 .15-.125.151.151 0 0 0-.15-.175H14.81Zm-.28-4c.63 0 1.032-.652.607-1.117A4.25 4.25 0 0 0 7.75 7.75v1.7c0 .094.003.186.009.278.022.335.317.572.653.572h.108c.426 0 .73-.423.73-.85v-1.7a2.75 2.75 0 0 1 4.589-2.045c.192.173.432.295.69.295Z" | |
clipRule="evenodd" | |
/> | |
<Path | |
stroke="currentColor" | |
strokeWidth={1.5} | |
d="M5.95 12.213c0-1.112.901-2.013 2.013-2.013h8.075c1.111 0 2.012.901 2.012 2.013V13.7a6.05 6.05 0 0 1-12.1 0v-1.487Z" | |
/> | |
<Circle cx={12} cy={13.7} r={1.375} stroke="currentColor" strokeWidth={1.5} /> | |
<Path stroke="currentColor" strokeLinecap="round" strokeWidth={1.5} d="M12 15.4v1.7" /> | |
</Svg> | |
)); | |
interface IPublicationHeaderProps extends Partial<PublicationDTO> { | |
hideFollowButton?: boolean; | |
hideAvatar?: boolean; | |
isOwnPublication?: boolean; | |
wrapperProps?: XStackProps; | |
isRetweet?: boolean; | |
} | |
const PublicationHeader = (props: IPublicationHeaderProps) => { | |
const router = useRouter(); | |
const { | |
Id, | |
FirstName, | |
LastName, | |
NickName, | |
ImageLink, | |
CreatorId, | |
Follow, | |
IsEncrypted, | |
NetworkType, | |
DateCreation, | |
hideFollowButton, | |
hideAvatar, | |
wrapperProps, | |
isRetweet, | |
isOwnPublication, | |
} = props; | |
const theme = useTheme(); | |
const initials = useMemo(() => { | |
return FirstName && LastName | |
? `${FirstName[0]}${LastName[0]}` | |
: NickName | |
? NickName.slice(0, 2) | |
: ''; | |
}, [FirstName, LastName, NickName]); | |
const fullNameMaybe = useMemo(() => { | |
const name = [FirstName, LastName].join(' ').trim(); | |
return name ? <Name>{name}</Name> : null; | |
}, [FirstName, LastName]); | |
const { decryptPost, decryptedText } = usePublicationDecrypt(); | |
return ( | |
<XStack {...wrapperProps}> | |
<XStack w="100%" ai="flex-start"> | |
<XStack gap={12}> | |
{hideAvatar ? null : ( | |
<UserAvatar | |
src={ImageLink} | |
size={32} | |
letter={initials} | |
onPress={() => | |
isOwnPublication | |
? router.replace({ | |
pathname: '/user', | |
}) | |
: router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: NickName, | |
}, | |
}) | |
} | |
/> | |
)} | |
<YStack> | |
<XStack ai="baseline"> | |
<TouchableOpacity | |
onPress={() => | |
isOwnPublication | |
? router.replace({ | |
pathname: '/user', | |
}) | |
: router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: NickName, | |
}, | |
}) | |
} | |
> | |
<XStack ai="center"> | |
{fullNameMaybe} | |
{isRetweet ? ( | |
<Nick fos={10} lh={12}> | |
{' '} | |
· reposted {getTimeAgo(DateCreation!)} | |
</Nick> | |
) : null} | |
</XStack> | |
<Nick>@{NickName}</Nick> | |
</TouchableOpacity> | |
</XStack> | |
</YStack> | |
</XStack> | |
<XStack ml="auto" ai="center"> | |
{IsEncrypted ? ( | |
<TouchableOpacity onPress={decryptPost}> | |
{decryptedText ? ( | |
<LockIconOpen color={theme.color.val} /> | |
) : ( | |
<LockIconClosed color={theme.color.val} /> | |
)} | |
</TouchableOpacity> | |
) : ( | |
<NetworkIcon networkType={NetworkType} style={{ marginRight: 5 }} /> | |
)} | |
{hideFollowButton ? null : <FollowButton CreatorId={CreatorId} Id={Id} Follow={Follow} />} | |
</XStack> | |
</XStack> | |
</XStack> | |
); | |
}; | |
PublicationHeader.displayName = 'PublicationHeader'; | |
/* ------------------------------------------------------------------------------------------------- | |
* FollowButton | |
* -----------------------------------------------------------------------------------------------*/ | |
const LogoIcon = memo((props: SvgProps) => ( | |
<Svg width={16} height={16} viewBox="0 0 16 16" fill="none" {...props}> | |
<G clipPath="url(#a)"> | |
<Path | |
fill="url(#b)" | |
d="M2.908 13.28c.8.8 1.69 1.378 2.672 1.734a6.429 6.429 0 0 0 2.977.36s1.248-.29 1.09-1.148c-.16-.856-1.364-.947-1.364-.947-1.514.166-2.83-.31-3.947-1.426-.982-.982-1.47-2.151-1.462-3.507.008-1.355.501-2.523 1.482-3.503.98-.98 2.15-1.477 3.51-1.489 1.36-.012 2.531.473 3.513 1.456 1.04 1.04 1.525 2.327 1.454 3.861 0 0-.034 1.107.933 1.254.968.147 1.147-.965 1.147-.965a6.735 6.735 0 0 0-.4-2.95c-.357-.973-.925-1.849-1.705-2.629C11.402 1.976 9.73 1.27 7.79 1.263c-1.939-.006-3.594.677-4.966 2.049C1.452 4.684.772 6.337.783 8.272c.01 1.933.72 3.603 2.125 5.009Z" | |
/> | |
<Path | |
fill="url(#c)" | |
stroke="url(#d)" | |
d="M5.403 10.738c.371.372.787.638 1.247.8a3 3 0 0 0 1.399.152s.588-.143.519-.546c-.07-.402-.636-.438-.636-.438-.714.087-1.33-.129-1.849-.648a2.18 2.18 0 0 1-.668-1.641c.012-.638.25-1.19.717-1.657a2.342 2.342 0 0 1 1.66-.72 2.173 2.173 0 0 1 1.645.664c.483.483.704 1.086.662 1.809 0 0-.023.52.431.584.455.064.546-.46.546-.46.047-.469-.01-.93-.172-1.386a3.275 3.275 0 0 0-.787-1.227c-.653-.653-1.436-.976-2.348-.968-.912.008-1.695.34-2.348.993-.654.653-.983 1.435-.99 2.344-.005.91.319 1.692.972 2.345Z" | |
/> | |
</G> | |
<Defs> | |
<LinearGradient | |
id="b" | |
x1={-3.738} | |
x2={9.187} | |
y1={11.104} | |
y2={14.658} | |
gradientUnits="userSpaceOnUse" | |
> | |
<Stop stopColor="#00D6D9" /> | |
<Stop offset={1} stopColor="#3C47C6" /> | |
</LinearGradient> | |
<LinearGradient | |
id="c" | |
x1={2.289} | |
x2={8.313} | |
y1={9.752} | |
y2={11.476} | |
gradientUnits="userSpaceOnUse" | |
> | |
<Stop stopColor="#00D6D9" /> | |
<Stop offset={1} stopColor="#3C47C6" /> | |
</LinearGradient> | |
<LinearGradient | |
id="d" | |
x1={2.289} | |
x2={8.313} | |
y1={9.752} | |
y2={11.476} | |
gradientUnits="userSpaceOnUse" | |
> | |
<Stop stopColor="#00D6D9" /> | |
<Stop offset={1} stopColor="#3C47C6" /> | |
</LinearGradient> | |
<ClipPath id="a"> | |
<Path fill="#fff" d="M0 0h16v16H0z" /> | |
</ClipPath> | |
</Defs> | |
</Svg> | |
)); | |
interface IFollowButtonProps extends Partial<PublicationDTO> {} | |
const FollowButton = memo((props: IFollowButtonProps) => { | |
const { colorMode } = useColorMode(); | |
const { follow, unfollow } = usePublicationActions({ userId: props.CreatorId }); | |
const { Follow, CreatorId, Id } = props; | |
const handleFollowUnfollow = useCallback(() => { | |
if (Follow) { | |
unfollow.mutate({ | |
queryParams: { | |
followingId: CreatorId, | |
}, | |
}); | |
} else { | |
follow.mutate({ | |
queryParams: { | |
followerUserId: CreatorId, | |
}, | |
}); | |
} | |
}, [Follow, CreatorId, Id, follow, unfollow]); | |
return ( | |
<> | |
<Button | |
onPress={handleFollowUnfollow} | |
// backgroundColor={colorMode === 'dark' ? '$background' : '#ecf0f9'} | |
borderRadius={4} | |
opacity={0.59} | |
px={6} | |
unstyled | |
fd="row" | |
ai="center" | |
jc="space-between" | |
iconAfter={<LogoIcon />} | |
height={24} | |
borderWidth={0} | |
color="$color" | |
> | |
{Follow ? 'Unfollow' : 'Follow'} | |
</Button> | |
</> | |
); | |
}); | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationText | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationTextProps extends Partial<PublicationDTO> {} | |
const PublicationText = (props: IPublicationTextProps) => { | |
const { colorMode } = useColorMode(); | |
const router = useRouter(); | |
const { Text, IsEncrypted, CanDecrypt } = props; | |
const { decryptedText } = usePublicationDecrypt(); | |
const plainText = useMemo(() => { | |
if (IsEncrypted && !decryptedText) return Text; | |
try { | |
if (decryptedText) { | |
return getPostTextFromEditorState( | |
JSON.parse(decryptedText) as unknown as RawDraftContentState | |
) as string; | |
} | |
return getPostTextFromEditorState( | |
JSON.parse(Text!) as unknown as RawDraftContentState | |
) as string; | |
} catch (_error) { | |
return ''; | |
} | |
}, [Text, IsEncrypted, decryptedText]) as string; | |
const onMentionHashtagPress = async (text: string) => { | |
if (text.startsWith('#')) { | |
console.log('Clicked to hashtag ' + text); | |
} | |
if (text.startsWith('@')) { | |
console.log('Clicked to mention ' + text); | |
const nickName = text.replace('@', ''); | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName, | |
}, | |
}); | |
} | |
if (text.startsWith('https://')) { | |
console.log('Clicked to link ' + text); | |
// TODO: Fix https://stackoverflow.com/questions/64699801/linking-canopenurl-returning-false-when-targeting-android-30-sdk-on-react-native | |
// const supported = await Linking.canOpenURL('https://twitter.com/elonmusk/status/1642984664091721733/'); | |
// console.log('Supported ' + supported); | |
// if (supported) { | |
// Opening the link with some app, if the URL colorMode is "http" the web link should be opened | |
// by some browser in the mobile | |
await Linking.openURL(text); | |
// } | |
} | |
}; | |
const onDecrypterPress = (text: string) => { | |
console.log('Clicked to decrypter ' + text); | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: text, | |
}, | |
}); | |
}; | |
const decrypters = useMemo(() => { | |
if (!CanDecrypt) return null; | |
const decryptersList = (CanDecrypt ?? '') | |
.split(',') | |
.filter((x) => !!x) | |
.map((x, i) => { | |
return ( | |
<Paragraph | |
key={i} | |
onPress={() => onDecrypterPress(x)} | |
color="#1f45c3" | |
fow="700" | |
bc="rgba(31,69,195,0.2)" | |
br={4} | |
mr={2} | |
> | |
@{x} | |
</Paragraph> | |
); | |
}); | |
return decryptersList; | |
}, [CanDecrypt, onDecrypterPress]); | |
if (!plainText) return null; | |
return ( | |
<YStack mt={8}> | |
{IsEncrypted && !decryptedText ? ( | |
<XStack ai="center"> | |
{decrypters} | |
<XStack ai="center" f={1}> | |
<Paragraph color="$paragraph" numberOfLines={1} ellipsizeMode="tail" f={1}> | |
{plainText} | |
</Paragraph> | |
<RNBlurView | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
bottom: 0, | |
right: 0, | |
}} | |
blurType="light" | |
blurRadius={1} | |
blurAmount={1} | |
overlayColor="transparent" | |
reducedTransparencyFallbackColor="white" | |
/> | |
</XStack> | |
</XStack> | |
) : ( | |
<MentionHashtagTextViewSmall mentionHashtagPress={onMentionHashtagPress}> | |
{plainText} | |
</MentionHashtagTextViewSmall> | |
)} | |
</YStack> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationDate | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationDateTimeProps extends Partial<PublicationDTO> {} | |
const PublicationDateTime = (props: IPublicationDateTimeProps) => { | |
const { DateCreation } = props; | |
const { date, time } = useMemo(() => { | |
return { | |
date: dayjs(DateCreation).format('DD.MM.YYYY'), | |
time: dayjs(DateCreation).format('HH:mm'), | |
}; | |
}, [DateCreation]); | |
return ( | |
<Paragraph color="#A4A3BB" fos={10} lh={12}> | |
{date}{' '} | |
<Paragraph fos={10} lh={12} color="$color"> | |
{time} | |
</Paragraph> | |
</Paragraph> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationTxLink | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationTxLinkProps extends Partial<PublicationDTO> {} | |
const PublicationTxLink = (props: IPublicationTxLinkProps) => { | |
const { NetworkType, BlockchainId, PublicationKey } = props; | |
const firstPublicationKey = String(PublicationKey).split(';')[0]?.trim(); | |
return ( | |
<ExternalLink url={getTransactionLink(NetworkType!, firstPublicationKey!, BlockchainId!)}> | |
<Paragraph fos={10} lh={12} color="rgba(94, 202, 244, 1)" textDecorationLine="underline"> | |
{getTransactionLink(NetworkType!, firstPublicationKey!, BlockchainId!).slice(0, 30).trim()} | |
... | |
</Paragraph> | |
</ExternalLink> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationRetweet | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationRetweetProps extends Partial<PublicationDTO> {} | |
export const PublicationRetweet = (props: IPublicationRetweetProps) => { | |
const { RetweetData, Retweet } = props; | |
if (!Retweet || !RetweetData) return null; | |
return ( | |
<PublicationLink id={RetweetData.Id!}> | |
<YStack | |
mt={8} | |
mb={1} | |
p={8} | |
style={{ | |
borderRadius: 8, | |
borderWidth: 1, | |
borderStyle: Platform.OS === 'ios' ? 'solid' : 'dashed', | |
borderColor: 'rgba(165,164,188,0.25)', | |
}} | |
> | |
<PublicationHeader {...RetweetData} hideFollowButton /> | |
<PublicationText {...RetweetData} /> | |
<PostingMedia IpfsFiles={RetweetData.IpfsFiles!} /> | |
</YStack> | |
</PublicationLink> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationActions | |
* -----------------------------------------------------------------------------------------------*/ | |
const XCenter = styled(XStack, { | |
alignItems: 'center', | |
justifyContent: 'center', | |
}); | |
const PostActionText = styled(Paragraph, { | |
name: 'PostActionText', | |
fontSize: 10, | |
fontWeight: '700', | |
lineHeight: 14, | |
ml: 2, | |
}); | |
const AnchorIcon = memo((props: SvgProps) => ( | |
<Svg width={16} height={16} fill="none" {...props}> | |
<Path | |
stroke="currentColor" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
d="M8.824 7.177a2.33 2.33 0 0 0-3.294 0L3.882 8.824a2.33 2.33 0 0 0 3.295 3.294L8 11.295" | |
/> | |
<Path | |
stroke="currentColor" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
d="M7.177 8.824a2.33 2.33 0 0 0 3.294 0l1.647-1.647a2.33 2.33 0 0 0-3.294-3.295L8 4.706" | |
/> | |
</Svg> | |
)); | |
interface IPublicationActionsProps extends Partial<PublicationDTO> {} | |
const PublicationActions = (props: IPublicationActionsProps) => { | |
const theme = useTheme(); | |
const { | |
Text, | |
IsEncrypted, | |
CommentCount, | |
Retweet, | |
RetweetCount, | |
RetweetData, | |
Id, | |
LikeCount, | |
Liked, | |
Favorite, | |
NetworkType, | |
IpfsFiles, | |
ViewsCount, | |
BlockchainId, | |
CreatorId, | |
} = props; | |
const router = useRouter(); | |
const { newLike, removeFromFavorites, addToFavorites, copyToClipboard } = usePublicationActions({ | |
userId: CreatorId, | |
}); | |
const hasRetweet = !!Retweet && RetweetData; | |
const hasVideo = IpfsFiles?.some((file) => file.FileType === CreatePostFileTypeEnum.video); | |
const plainText = useMemo(() => { | |
if (IsEncrypted) return Text; | |
try { | |
return getPostTextFromEditorState( | |
JSON.parse(Text!) as unknown as RawDraftContentState | |
) as string; | |
} catch (_error) { | |
return ''; | |
} | |
}, [Text, IsEncrypted]) as string; | |
const handleLike = () => { | |
newLike.mutate({ | |
body: { | |
BlockchainId, | |
LikeObjectId: Id, | |
}, | |
}); | |
}; | |
const handleFavorite = () => { | |
if (Favorite) { | |
removeFromFavorites.mutate({ | |
queryParams: { | |
publicationId: Id, | |
}, | |
}); | |
} else { | |
addToFavorites.mutate({ | |
queryParams: { | |
publicationId: Id, | |
}, | |
}); | |
} | |
}; | |
return ( | |
<XStack jc="space-between" mt={12}> | |
<XStack> | |
<TouchableOpacity | |
style={{ marginRight: 1, padding: 5, borderRadius: 5 }} | |
onPress={() => { | |
router.push({ | |
pathname: '/comment/new/[postId]', | |
query: { | |
postId: Id, | |
}, | |
}); | |
}} | |
> | |
<XCenter> | |
{/* @ts-ignore */} | |
<IconComment color={theme.color.val} /> | |
<PostActionText>{CommentCount}</PostActionText> | |
</XCenter> | |
</TouchableOpacity> | |
<TouchableOpacity | |
style={{ marginRight: 1, padding: 5, borderRadius: 5 }} | |
onPress={() => { | |
router.push({ | |
pathname: '/repost/new/[postId]', | |
query: { | |
postId: hasRetweet ? RetweetData.Id : Id, | |
}, | |
}); | |
}} | |
> | |
<XCenter> | |
{/* @ts-ignore */} | |
<IconRetweet color={theme.color.val} /> | |
<PostActionText>{RetweetCount}</PostActionText> | |
</XCenter> | |
</TouchableOpacity> | |
<TouchableOpacity | |
style={{ marginRight: 1, padding: 5, borderRadius: 5 }} | |
onPress={handleLike} | |
> | |
<XCenter> | |
{/* @ts-ignore */} | |
<IconLike color={theme.color.val} focused={Boolean(Liked)} /> | |
<PostActionText>{LikeCount}</PostActionText> | |
</XCenter> | |
</TouchableOpacity> | |
{hasVideo && ( | |
<XCenter mr={1} p={5}> | |
{/* @ts-ignore */} | |
<IconView color={theme.color.val} /> | |
<PostActionText>{ViewsCount}</PostActionText> | |
</XCenter> | |
)} | |
<TouchableOpacity | |
style={{ marginRight: 1, padding: 5, borderRadius: 5 }} | |
onPress={handleFavorite} | |
> | |
{/* @ts-ignore */} | |
<IconBookmark color={theme.color.val} focused={Boolean(Favorite)} /> | |
</TouchableOpacity> | |
<TouchableOpacity | |
style={{ marginRight: 1, padding: 5, borderRadius: 5 }} | |
onPress={() => copyToClipboard(Config.API_URL + `/post/${Id}`)} | |
> | |
{/* @ts-ignore */} | |
<IconCopy color={theme.color.val} /> | |
</TouchableOpacity> | |
</XStack> | |
<XStack> | |
<ExternalLink url={getOpenSeaLink(NetworkType!, BlockchainId!)}> | |
<XStack p={5}> | |
{/* @ts-ignore */} | |
<IconOpensea color={theme.color.val} /> | |
</XStack> | |
</ExternalLink> | |
</XStack> | |
</XStack> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationThreadWrapper | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationThreadWrapperProps extends Partial<PublicationDTO> { | |
children: React.ReactNode; | |
} | |
const PublicationThreadWrapper = (props: IPublicationThreadWrapperProps) => { | |
const { Id, IsThread, ImageLink, FirstName, LastName, NickName, children } = props; | |
const router = useRouter(); | |
const initials = useMemo(() => { | |
return FirstName && LastName | |
? `${FirstName[0]}${LastName[0]}` | |
: NickName | |
? NickName.slice(0, 2) | |
: ''; | |
}, [FirstName, LastName, NickName]); | |
if (!IsThread) return <>{children}</>; | |
return ( | |
<> | |
<XStack> | |
<YStack ai="center" width={30}> | |
<YStack f={1} w={1} backgroundColor="$color" opacity={0.15} /> | |
</YStack> | |
<YStack f={1} pb={10}> | |
{children} | |
</YStack> | |
</XStack> | |
<XStack ai="center" gap={6}> | |
<XStack w={30} ai="center" jc="center"> | |
<UserAvatar | |
src={ImageLink} | |
size={24} | |
letter={initials} | |
onPress={() => | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: NickName, | |
}, | |
}) | |
} | |
/> | |
</XStack> | |
<TouchableOpacity | |
onPress={() => { | |
router.push({ | |
pathname: '/thread/[id]', | |
query: { | |
id: Id, | |
}, | |
}); | |
}} | |
> | |
<Paragraph color="#3C47C6">Show this thread</Paragraph> | |
</TouchableOpacity> | |
</XStack> | |
</> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationCell | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationCellProps extends YStackProps { | |
publication: PublicationDTO; | |
} | |
const PublicationCell = (props: IPublicationCellProps) => { | |
const { currentUser } = useAuth(); | |
const { publication, ...rest } = props; | |
const isOwnPublication = currentUser?.Id === publication?.CreatorId; | |
const publicationType = publication?.IsThread ? 'thread' : 'post'; | |
const isRetweet = !!publication?.Retweet; | |
return ( | |
<ErrorHandler FallbackComponent={null}> | |
<Suspense fallback={<PublicationSkeleton />}> | |
<PublicationDecryptProvider publication={publication}> | |
<PublicationLink id={publication.Id!} type={publicationType}> | |
<YStack py={16} {...rest}> | |
<PublicationHeader | |
{...publication} | |
hideFollowButton={isOwnPublication} | |
isRetweet={isRetweet} | |
isOwnPublication={isOwnPublication} | |
/> | |
<PublicationThreadWrapper {...publication}> | |
<PublicationText {...publication} /> | |
<PublicationRetweet {...publication} /> | |
<PostingMedia IpfsFiles={publication.IpfsFiles!} /> | |
<XStack ai="center" jc="space-between" mt={4}> | |
<PublicationDateTime {...publication} /> | |
<PublicationTxLink {...publication} /> | |
</XStack> | |
<PublicationActions {...publication} /> | |
</PublicationThreadWrapper> | |
</YStack> | |
</PublicationLink> | |
</PublicationDecryptProvider> | |
</Suspense> | |
</ErrorHandler> | |
); | |
}; | |
PublicationCell.displayName = 'PublicationCell'; | |
export default memo(PublicationCell); | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationDetailsText | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationDetailsTextProps extends Partial<PublicationDTO> { | |
textStyle?: StyleProp<TextStyle>; | |
} | |
const PublicationDetailsText = (props: IPublicationDetailsTextProps) => { | |
const { colorMode } = useColorMode(); | |
const router = useRouter(); | |
const { Text, IsEncrypted, CanDecrypt, textStyle } = props; | |
const { decryptedText } = usePublicationDecrypt(); | |
const plainText = useMemo(() => { | |
if (IsEncrypted && !decryptedText) return Text; | |
try { | |
if (decryptedText) { | |
return getPostTextFromEditorState( | |
JSON.parse(decryptedText) as unknown as RawDraftContentState | |
) as string; | |
} | |
return getPostTextFromEditorState( | |
JSON.parse(Text!) as unknown as RawDraftContentState | |
) as string; | |
} catch (_error) { | |
return ''; | |
} | |
}, [Text, IsEncrypted, decryptedText]) as string; | |
const onMentionHashtagPress = async (text: string) => { | |
if (text.startsWith('#')) { | |
console.log('Clicked to hashtag ' + text); | |
} | |
if (text.startsWith('@')) { | |
console.log('Clicked to mention ' + text); | |
const nickName = text.replace('@', ''); | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName, | |
}, | |
}); | |
} | |
if (text.startsWith('https://')) { | |
console.log('Clicked to link ' + text); | |
// TODO: Fix https://stackoverflow.com/questions/64699801/linking-canopenurl-returning-false-when-targeting-android-30-sdk-on-react-native | |
// const supported = await Linking.canOpenURL('https://twitter.com/elonmusk/status/1642984664091721733/'); | |
// console.log('Supported ' + supported); | |
// if (supported) { | |
// Opening the link with some app, if the URL colorMode is "http" the web link should be opened | |
// by some browser in the mobile | |
await Linking.openURL(text); | |
// } | |
} | |
}; | |
const onDecrypterPress = (text: string) => { | |
console.log('Clicked to decrypter ' + text); | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: text, | |
}, | |
}); | |
}; | |
const decrypters = useMemo(() => { | |
if (!CanDecrypt) return null; | |
const decryptersList = (CanDecrypt ?? '') | |
.split(',') | |
.filter((x) => !!x) | |
.map((x, i) => { | |
return ( | |
<Paragraph | |
key={i} | |
onPress={() => onDecrypterPress(x)} | |
color="#1f45c3" | |
fow="700" | |
bc="rgba(31,69,195,0.2)" | |
br={4} | |
mr={2} | |
> | |
@{x} | |
</Paragraph> | |
); | |
}); | |
return decryptersList; | |
}, [CanDecrypt, onDecrypterPress]); | |
if (!plainText) return null; | |
return ( | |
<YStack mt={8}> | |
{IsEncrypted && !decryptedText ? ( | |
<XStack ai="center"> | |
{decrypters} | |
<XStack ai="center" f={1}> | |
<Paragraph | |
fos={16} | |
color="$paragraph" | |
numberOfLines={1} | |
ellipsizeMode="tail" | |
ellipse | |
f={1} | |
> | |
{plainText} | |
</Paragraph> | |
<RNBlurView | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
bottom: 0, | |
right: 0, | |
}} | |
blurType="xlight" | |
blurRadius={1} | |
blurAmount={1} | |
overlayColor="transparent" | |
reducedTransparencyFallbackColor="white" | |
/> | |
</XStack> | |
</XStack> | |
) : ( | |
<MentionHashtagTextViewFull | |
mentionHashtagPress={onMentionHashtagPress} | |
fontSize={16} | |
lh={22} | |
fontWeight="700" | |
color="$color" | |
style={textStyle} | |
> | |
{plainText} | |
</MentionHashtagTextViewFull> | |
)} | |
</YStack> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationBacking | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationBackingProps extends Partial<PublicationDTO> {} | |
const PublicationBacking = (props: IPublicationBackingProps) => { | |
const { setCurrentBackingPublicationId } = useBackPost(); | |
const { Id, BackedCount } = props; | |
const handleBackPostPress = () => { | |
setCurrentBackingPublicationId(Id); | |
}; | |
return ( | |
<TouchableOpacity onPress={handleBackPostPress}> | |
<XStack ai="center" jc="flex-start" mt={8} gap={4}> | |
<Paragraph fos={10} color="$secondary"> | |
Backed by:{' '} | |
<Paragraph fos={12} fontWeight="700"> | |
{BackedCount} | |
</Paragraph> | |
</Paragraph> | |
<LogoIcon width={12} height={12} /> | |
</XStack> | |
</TouchableOpacity> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* PublicationDetails | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IPublicationDetailsProps extends YStackProps { | |
publication: PublicationDTO; | |
} | |
export const PublicationDetails = (props: IPublicationDetailsProps) => { | |
const { publication, ...rest } = props; | |
const { currentUser } = useAuth(); | |
const isOwnPublication = currentUser?.Id === publication?.CreatorId; | |
return ( | |
<ErrorHandler FallbackComponent={null}> | |
<Suspense fallback={<PublicationSkeleton />}> | |
<PublicationDecryptProvider publication={publication}> | |
<YStack py={16} {...rest}> | |
<PublicationHeader {...publication} hideFollowButton={isOwnPublication} /> | |
<PublicationBacking {...publication} /> | |
<PublicationDetailsText {...publication} /> | |
<PublicationRetweet {...publication} /> | |
<PostingMedia IpfsFiles={publication.IpfsFiles!} /> | |
<XStack ai="center" jc="space-between" mt={4}> | |
<PublicationDateTime {...publication} /> | |
<PublicationTxLink {...publication} /> | |
</XStack> | |
<PublicationActions {...publication} /> | |
</YStack> | |
</PublicationDecryptProvider> | |
</Suspense> | |
</ErrorHandler> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* ThreadPublicationDetails | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IThreadPublicationDetailsProps extends YStackProps { | |
publication: PublicationDTO; | |
type: 'head' | 'body' | 'tail'; | |
} | |
export const ThreadPublicationDetails = (props: IThreadPublicationDetailsProps) => { | |
const { currentUser } = useAuth(); | |
const { publication, type, ...rest } = props; | |
const shouldHideFollowButton = currentUser?.Id === publication?.CreatorId || type !== 'head'; | |
const textStyle = type !== 'head' ? { fontSize: 12, lineHeight: 19, fontWeight: 'normal' } : {}; | |
return ( | |
<ErrorHandler FallbackComponent={null}> | |
<Suspense fallback={<PublicationSkeleton />}> | |
<PublicationLink id={publication.Id!}> | |
<YStack {...rest}> | |
<ThreadPublicationSideWrapper {...publication} type={type}> | |
<PublicationDecryptProvider publication={publication}> | |
<PublicationHeader | |
{...publication} | |
hideFollowButton={shouldHideFollowButton} | |
hideAvatar | |
/> | |
<PublicationDetailsText {...publication} textStyle={textStyle as TextStyle} /> | |
<PublicationRetweet {...publication} /> | |
<PostingMedia IpfsFiles={publication.IpfsFiles!} /> | |
<XStack ai="center" jc="space-between" mt={4}> | |
<PublicationDateTime {...publication} /> | |
<PublicationTxLink {...publication} /> | |
</XStack> | |
<PublicationActions {...publication} /> | |
</PublicationDecryptProvider> | |
</ThreadPublicationSideWrapper> | |
</YStack> | |
</PublicationLink> | |
</Suspense> | |
</ErrorHandler> | |
); | |
}; | |
/* ------------------------------------------------------------------------------------------------- | |
* ThreadPublicationSideWrapper | |
* -----------------------------------------------------------------------------------------------*/ | |
interface IThreadPublicationSideWrapperProps extends Partial<PublicationDTO> { | |
children: React.ReactNode; | |
type: 'head' | 'body' | 'tail'; | |
} | |
const ThreadPublicationSideWrapper = (props: IThreadPublicationSideWrapperProps) => { | |
const { Id, IsThread, ImageLink, FirstName, LastName, NickName, children, type } = props; | |
const router = useRouter(); | |
const initials = useMemo(() => { | |
return FirstName && LastName | |
? `${FirstName[0]}${LastName[0]}` | |
: NickName | |
? NickName.slice(0, 2) | |
: ''; | |
}, [FirstName, LastName, NickName]); | |
const avatarSize = type === 'head' ? 32 : 24; | |
return ( | |
<> | |
<XStack> | |
<YStack ai="center" width={32} pt={type === 'head' ? 16 : 0}> | |
<YStack pos="absolute" top={16}> | |
<UserAvatar | |
src={ImageLink} | |
size={avatarSize} | |
letter={initials} | |
onPress={() => | |
router.push({ | |
pathname: '/user/[nickName]', | |
query: { | |
nickName: NickName, | |
}, | |
}) | |
} | |
/> | |
</YStack> | |
<YStack | |
f={1} | |
w={1} | |
backgroundColor="$color" | |
opacity={0.15} | |
maxHeight={type === 'tail' ? 16 : undefined} | |
/> | |
</YStack> | |
<YStack | |
f={1} | |
pt={16} | |
pb={10} | |
pl={8} | |
borderBottomWidth={type === 'tail' ? 0 : 1} | |
borderBottomColor="rgba(165,164,188,0.25)" | |
borderStyle={Platform.OS === 'ios' ? 'solid' : 'dashed'} | |
> | |
{children} | |
</YStack> | |
</XStack> | |
</> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CanActivate, | |
ExecutionContext, | |
ForbiddenException, | |
Injectable, | |
} from '@nestjs/common'; | |
import { Observable } from 'rxjs'; | |
import { Reflector } from '@nestjs/core'; | |
import { ROLES_KEY } from './roles-auth.decorator'; | |
import { User } from 'src/users/entities/user.entity'; | |
import { Role as RoleEnum } from './role.enum'; | |
@Injectable() | |
export class RolesGuard implements CanActivate { | |
constructor(private reflector: Reflector) {} | |
canActivate( | |
context: ExecutionContext, | |
): boolean | Promise<boolean> | Observable<boolean> { | |
try { | |
const requiredRoles = this.reflector.getAllAndOverride<RoleEnum[]>( | |
ROLES_KEY, | |
[context.getHandler(), context.getClass()], | |
); | |
if (!requiredRoles) { | |
return true; | |
} | |
const req = context.switchToHttp().getRequest(); | |
const user: User = req.user; | |
return user.roles.some((role) => | |
requiredRoles.includes(role.value as RoleEnum), | |
); | |
} catch (e) { | |
console.log(e); | |
throw new ForbiddenException(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as os from 'os'; | |
const { parentPort } = require('worker_threads'); | |
import * as Parser from 'rss-parser'; | |
import { createConnection, Repository } from "typeorm"; | |
let parser = new Parser(); | |
import * as Cabin from 'cabin'; | |
import * as pMap from 'p-map'; | |
import * as puppeteer from 'puppeteer'; | |
import Bill from '../entity/Bill'; | |
import Document from '../entity/Document'; | |
import Category from '../entity/Category'; | |
const logger = new Cabin(); | |
let isCancelled = false; | |
const concurrency = os.cpus().length; | |
interface IRssItem { | |
title: string; | |
link: string; | |
contentSnippet: string; | |
categories: string[]; | |
} | |
const rssLinks = [ | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Administrat%C4%ABv%C4%81s%20ties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Bud%C5%BEets%2C%20nodok%C4%BCi%2C%20nodevas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Cilv%C4%93kties%C4%ABbas%2C%20b%C4%93rnu%20ties%C4%ABbas%2C%20ties%C4%ABbsargs', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Civilties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Darba%20ties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Lietved%C4%ABba%2C%20arh%C4%ABvi%2C%20dati', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=IIzgl%C4%ABt%C4%ABba%2C%20zin%C4%81tne%2C%20kult%C5%ABra%2C%20sports', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Komercties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Krimin%C4%81lties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Lauksaimniec%C4%ABba%2C%20me%C5%BEsaimniec%C4%ABba%2C%20zivsaimniec%C4%ABba', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Vide%2C%20dabas%20aizsardz%C4%ABba', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Mediji%2C%20rekl%C4%81ma%2C%20intelektu%C4%81lais%20%C4%ABpa%C5%A1ums', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Muita%2C%20valsts%20robe%C5%BEa', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Nekustamais%20%C4%ABpa%C5%A1ums%2C%20b%C5%ABvniec%C4%ABba', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Finan%C5%A1u%20tirgus%2C%20bankas%2C%20apdro%C5%A1in%C4%81%C5%A1ana%2C%20azartsp%C4%93les%2C%20izlozes', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Re%C4%A3ion%C4%81l%C4%81%20politika', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Pilson%C4%ABba%2C%20imigr%C4%81cija%2C%20patv%C4%93rums%2C%20personas%20statuss', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Biedr%C4%ABbas%20un%20nodibin%C4%81jumi%2C%20reli%C4%A3ija', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Soci%C4%81l%C4%81%20aizsardz%C4%ABba%2C%20vesel%C4%ABba', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Starptautisk%C4%81%20sadarb%C4%ABba%2C%20ES', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Tiesu%20vara%2C%20policija%2C%20korupcijas%20nov%C4%93r%C5%A1ana', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=P%C4%81rtika%2C%20tirdzniec%C4%ABba%2C%20pat%C4%93r%C4%93t%C4%81ju%20ties%C4%ABbas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=T%C5%ABrisms%2C%20transports%2C%20ostas%2C%20sakari%2C%20ener%C4%A3%C4%93tika', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Valsts%20dro%C5%A1%C4%ABba%20un%20aizsardz%C4%ABba', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Valsts%20instit%C5%ABcijas%2C%20civildienests', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Valsts%20simboli%2C%20valoda%2C%20sv%C4%93tki', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Konstitucion%C4%81l%C4%81s%20ties%C4%ABbas%2C%20v%C4%93l%C4%93%C5%A1anas', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=Publiskais%20iepirkums', | |
'http://titania.saeima.lv/LIVS13/saeimalivs13.nsf/livs.rss?openagent&cat=COVID%20-19%20', | |
] | |
const preloadBillByName = async (name: string, billRepository: Repository<Bill>) => { | |
const existingBill = await billRepository.findOne({ name }) | |
if (existingBill) return existingBill; | |
return billRepository.create({ name }) | |
} | |
const preloadCategoryByName = async (name: string, categoryRepository: Repository<Category>) => { | |
const existingCategory = await categoryRepository.findOne({ name }) | |
if (existingCategory) return existingCategory; | |
return categoryRepository.create({ name }) | |
} | |
async function mapper( | |
item: IRssItem, | |
browser: puppeteer.Browser, | |
billRepository: Repository<Bill>, | |
documentRepository: Repository<Document>, | |
categoryRepository: Repository<Category> | |
) { | |
// return early if the job was already cancelled | |
if (isCancelled) return; | |
const page = await browser.newPage(); | |
try { | |
await page.goto(item.link); | |
await page.content(); | |
const documentLinks = await page.evaluate((sel: string) => { | |
const elements: HTMLAnchorElement[] = Array.from(document.querySelectorAll(sel)); | |
return elements.map(element => { | |
return { | |
name: element.innerText.trim(), | |
content: element.href, | |
} | |
}) | |
}, '.embView > .docListBlock div:nth-child(2) a'); | |
const pages = documentLinks.map(async (dl) => { | |
const existingDocument = await documentRepository.findOne({ name: dl.name }) | |
if (existingDocument) return existingDocument; | |
const newPage = await browser.newPage(); | |
try { | |
await newPage.goto(dl.content); | |
const content = await newPage.content(); | |
const doc = { | |
name: dl.name, | |
content | |
}; | |
return documentRepository.create({ ...doc }) | |
} catch (error) { | |
console.log(error) | |
} finally { | |
await newPage.close(); | |
} | |
}) | |
const bill = await preloadBillByName( | |
`${item.title} ${item.contentSnippet}`, | |
billRepository | |
); | |
const documentPages = await Promise.all(pages); | |
bill.documents = documentPages; | |
bill.category = await preloadCategoryByName(item.categories[0], categoryRepository); | |
await billRepository.save(bill) | |
return { [item.title.trim()]: documentLinks }; | |
} catch (err) { | |
logger.error(err); | |
} finally { | |
await page.close() | |
} | |
} | |
if (parentPort) | |
parentPort.once('message', message => { | |
if (message === 'cancel') isCancelled = true; | |
}); | |
(async () => { | |
const connection = await createConnection(); | |
const billRepository = connection.getRepository(Bill); | |
const documentRepository = connection.getRepository(Document); | |
const categoryRepository = connection.getRepository(Category); | |
const allFeed = await pMap(rssLinks, async (url) => await parser.parseURL(url), { concurrency }) | |
const items = allFeed.reduce((acc, curr) => { | |
return [...acc, ...curr.items] | |
}, []) | |
const browser = await puppeteer.launch({ | |
executablePath: 'google-chrome-stable', | |
args: [ | |
'--no-sandbox', | |
'--disable-setuid-sandbox' | |
] | |
}); | |
await pMap( | |
items, | |
(item: IRssItem) => mapper( | |
item, | |
browser, | |
billRepository, | |
documentRepository, | |
categoryRepository | |
), | |
{ concurrency } | |
); | |
await browser.close() | |
// signal to parent that the job is done | |
if (parentPort) parentPort.postMessage('done'); | |
else process.exit(0); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment