Skip to content

Commit 976e83f

Browse files
committed
feat: import contacts file
1 parent 51c4176 commit 976e83f

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

apps/api/src/chat/resolvers/contact.resolver.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getXmtpContacts } from '@chainjet/tools/dist/messages'
77
import { BadRequestException, Logger, UnauthorizedException, UseGuards, UseInterceptors } from '@nestjs/common'
88
import { Args, Mutation, Resolver } from '@nestjs/graphql'
99
import { Authorizer, AuthorizerInterceptor, InjectAuthorizer } from '@ptc-org/nestjs-query-graphql'
10+
import AWS from 'aws-sdk'
1011
import { getAddress, isAddress } from 'ethers/lib/utils'
1112
import { GraphQLBoolean, GraphQLString } from 'graphql'
1213
import { ObjectId } from 'mongoose'
@@ -19,6 +20,13 @@ import { UserService } from '../../users/services/user.service'
1920
import { Contact, CreateContactInput, UpdateContactInput } from '../entities/contact'
2021
import { ContactService } from '../services/contact.service'
2122

23+
AWS.config.update({
24+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
25+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
26+
region: 'us-east-2',
27+
})
28+
const s3 = new AWS.S3()
29+
2230
@Resolver(() => Contact)
2331
@UseGuards(GraphqlGuard)
2432
@UseInterceptors(AuthorizerInterceptor(Contact))
@@ -69,6 +77,85 @@ export class ContactResolver extends BaseResolver(Contact, {
6977
}
7078
}
7179

80+
@Mutation(() => GraphQLString)
81+
async generateContactsPresignedUrl(
82+
@UserId() userId: ObjectId,
83+
@Args({ name: 'id', type: () => GraphQLString }) id: string,
84+
): Promise<string> {
85+
if (!userId) {
86+
throw new UnauthorizedException('Not logged in')
87+
}
88+
const user = await this.userService.findOne({ _id: userId })
89+
if (!user) {
90+
throw new BadRequestException('User not found')
91+
}
92+
const threeDays = new Date()
93+
threeDays.setDate(threeDays.getDate() + 3)
94+
const params = {
95+
Bucket: 'chainjet-contacts',
96+
Fields: {
97+
key: `${userId}/${id}.txt`,
98+
},
99+
Expires: 60,
100+
Metadata: {
101+
Expires: threeDays.toISOString(),
102+
},
103+
Conditions: [['content-length-range', 0, 52428800]], // up to 50MB
104+
}
105+
return new Promise((resolve, reject) => {
106+
s3.createPresignedPost(params, function (err, data) {
107+
if (err) {
108+
reject(err)
109+
} else {
110+
resolve(JSON.stringify(data))
111+
}
112+
})
113+
})
114+
}
115+
116+
@Mutation(() => ResultPayload)
117+
async addContactsFile(
118+
@UserId() userId: ObjectId,
119+
@Args({ name: 'id', type: () => GraphQLString }) id: string,
120+
@Args({ name: 'tags', type: () => [GraphQLString], nullable: true }) tags?: string[],
121+
@Args({ name: 'limitToPlan', type: () => GraphQLBoolean, nullable: true }) limitToPlan?: boolean,
122+
): Promise<ResultPayload> {
123+
if (!userId) {
124+
throw new UnauthorizedException('Not logged in')
125+
}
126+
const user = await this.userService.findOne({ _id: userId })
127+
if (!user) {
128+
throw new BadRequestException('User not found')
129+
}
130+
131+
const params = {
132+
Bucket: 'chainjet-contacts',
133+
Key: `${userId}/${id}.txt`,
134+
}
135+
136+
let fileContent = ''
137+
try {
138+
const data = await s3.getObject(params).promise()
139+
fileContent = data.Body?.toString('utf-8') || ''
140+
} catch (error) {
141+
throw new Error(`Failed to import the contacts: ${error.message}`)
142+
}
143+
const addresses = fileContent.split('\n')
144+
if (!addresses || addresses.length === 0) {
145+
throw new BadRequestException('Addresses cannot be empty')
146+
}
147+
if (addresses.some((address) => !address || !isAddress(address))) {
148+
throw new BadRequestException(
149+
`Address ${addresses.find((address) => !address || !isAddress(address))} is not valid`,
150+
)
151+
}
152+
this.logger.log(`Importing ${addresses.length} addresses - user ${user.id}`)
153+
await this.contactService.addContacts(addresses, user, tags, limitToPlan)
154+
return {
155+
success: true,
156+
}
157+
}
158+
72159
@Mutation(() => ResultPayload)
73160
async importXmtpContacts(
74161
@UserId() userId: ObjectId,

generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,6 +1703,8 @@ export interface IMutation {
17031703
updateOneContact(input: UpdateOneContactInput): Contact | Promise<Contact>;
17041704
deleteOneContact(input: DeleteOneContactInput): ContactDeleteResponse | Promise<ContactDeleteResponse>;
17051705
addContacts(addresses: string[], tags?: Nullable<string[]>, limitToPlan?: Nullable<boolean>): ResultPayload | Promise<ResultPayload>;
1706+
generateContactsPresignedUrl(id: string): string | Promise<string>;
1707+
addContactsFile(id: string, tags?: Nullable<string[]>, limitToPlan?: Nullable<boolean>): ResultPayload | Promise<ResultPayload>;
17061708
importXmtpContacts(tags?: Nullable<string[]>, limitToPlan?: Nullable<boolean>): ResultPayload | Promise<ResultPayload>;
17071709
importPoapContacts(eventId: string, tags?: Nullable<string[]>, limitToPlan?: Nullable<boolean>): ResultPayload | Promise<ResultPayload>;
17081710
importLensFollowersContacts(handle: string, tags?: Nullable<string[]>, limitToPlan?: Nullable<boolean>): ResultPayload | Promise<ResultPayload>;

generated/schema.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,8 @@ type Mutation {
18231823
updateOneContact(input: UpdateOneContactInput!): Contact!
18241824
deleteOneContact(input: DeleteOneContactInput!): ContactDeleteResponse!
18251825
addContacts(addresses: [String!]!, tags: [String!], limitToPlan: Boolean): ResultPayload!
1826+
generateContactsPresignedUrl(id: String!): String!
1827+
addContactsFile(id: String!, tags: [String!], limitToPlan: Boolean): ResultPayload!
18261828
importXmtpContacts(tags: [String!], limitToPlan: Boolean): ResultPayload!
18271829
importPoapContacts(eventId: String!, tags: [String!], limitToPlan: Boolean): ResultPayload!
18281830
importLensFollowersContacts(handle: String!, tags: [String!], limitToPlan: Boolean): ResultPayload!

0 commit comments

Comments
 (0)