Skip to content

Commit bd70831

Browse files
committed
feat: add and remove tags
1 parent e4756ce commit bd70831

File tree

9 files changed

+203
-34
lines changed

9 files changed

+203
-34
lines changed

apps/api/src/contacts/services/contact.service.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BaseService } from '@app/common/base/base.service'
2+
import { Reference } from '@app/common/typings/mongodb'
23
import {
34
getWalletEns,
45
getWalletFarcasterProfile,
@@ -7,16 +8,20 @@ import {
78
} from '@app/definitions/utils/address.utils'
89
import { Injectable, Logger } from '@nestjs/common'
910
import { ReturnModelType } from '@typegoose/typegoose'
11+
import { uniq } from 'lodash'
12+
import { ObjectId } from 'mongodb'
1013
import { InjectModel } from 'nestjs-typegoose'
1114
import { User } from '../../users/entities/user'
1215
import { Contact } from '../entities/contact'
1316

1417
@Injectable()
1518
export class ContactService extends BaseService<Contact> {
1619
protected readonly logger = new Logger(ContactService.name)
20+
static instance: ContactService
1721

1822
constructor(@InjectModel(Contact) protected readonly model: ReturnModelType<typeof Contact>) {
1923
super(model)
24+
ContactService.instance = this
2025
}
2126

2227
async resolveContactData(address: string, key: string, contactOwner: User) {
@@ -34,4 +39,49 @@ export class ContactService extends BaseService<Contact> {
3439
return contact?.tags ?? []
3540
}
3641
}
42+
43+
async addTags(address: string, tags: string[], ownerId: ObjectId): Promise<string[]> {
44+
const contact = await this.findOne({
45+
owner: ownerId,
46+
address,
47+
})
48+
if (!contact) {
49+
await this.createOne({
50+
owner: ownerId as Reference<User>,
51+
address,
52+
tags,
53+
})
54+
return tags
55+
}
56+
if (tags.length) {
57+
const newTags = uniq([...contact.tags, ...tags])
58+
if (newTags.length !== contact.tags.length) {
59+
await this.updateById(contact._id, {
60+
tags: newTags,
61+
})
62+
return newTags
63+
}
64+
}
65+
return contact.tags
66+
}
67+
68+
async removeTags(address: string, tags: string[], ownerId: ObjectId): Promise<string[]> {
69+
const contact = await this.findOne({
70+
owner: ownerId,
71+
address,
72+
})
73+
if (!contact) {
74+
return []
75+
}
76+
if (tags.length) {
77+
const newTags = contact.tags.filter((tag) => !tags.includes(tag))
78+
if (newTags.length !== contact.tags.length) {
79+
await this.updateById(contact._id, {
80+
tags: newTags,
81+
})
82+
return newTags
83+
}
84+
}
85+
return contact.tags
86+
}
3787
}

apps/api/src/workflow-triggers/controllers/chatbot.controller.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { XmtpMessageOutput } from '@app/definitions/integration-definitions/xmtp/xmtp.common'
22
import { BadRequestException, Body, Controller, Logger, Post, Req, UnauthorizedException } from '@nestjs/common'
33
import { Request } from 'express'
4-
import { uniq } from 'lodash'
54
import { ObjectId } from 'mongodb'
65
import { Types } from 'mongoose'
76
import { RunnerService } from '../../../../runner/src/services/runner.service'
@@ -162,25 +161,9 @@ export class ChatbotController {
162161

163162
// TODO apply activateForNewConversations
164163

165-
const tags = workflowTrigger.inputs?.tags?.split(',').map((tag) => tag.trim()) ?? []
166-
const contact = await this.contactService.findOne({
167-
owner: workflow.owner,
168-
address: message.senderAddress,
169-
})
170-
if (!contact) {
171-
await this.contactService.createOne({
172-
owner: workflow.owner,
173-
address: message.senderAddress,
174-
tags,
175-
})
176-
} else if (workflowTrigger.inputs?.tags) {
177-
const newTags = uniq([...contact.tags, ...tags])
178-
if (newTags.length !== contact.tags.length) {
179-
await this.contactService.updateById(contact._id, {
180-
tags: contact.tags,
181-
})
182-
}
183-
}
164+
// add tags to the contact
165+
const tags: string[] = workflowTrigger.inputs?.tags?.split(',').map((tag: string) => tag.trim()) ?? []
166+
await this.contactService.addTags(message.senderAddress, tags, workflow.owner._id)
184167

185168
const hookTriggerOutputs = {
186169
id: message.id,

generated/graphql.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,13 @@ export interface IntegrationTriggersConnection {
11071107
edges: IntegrationTriggerEdge[];
11081108
}
11091109

1110+
export interface Contact {
1111+
id: string;
1112+
createdAt: DateTime;
1113+
address: string;
1114+
tags?: Nullable<string[]>;
1115+
}
1116+
11101117
export interface AccountCredential {
11111118
id: string;
11121119
createdAt: DateTime;
@@ -1207,13 +1214,6 @@ export interface UserEdge {
12071214
cursor: ConnectionCursor;
12081215
}
12091216

1210-
export interface Contact {
1211-
id: string;
1212-
createdAt: DateTime;
1213-
address: string;
1214-
tags?: Nullable<string[]>;
1215-
}
1216-
12171217
export interface ContactDeleteResponse {
12181218
id?: Nullable<string>;
12191219
createdAt?: Nullable<DateTime>;

generated/schema.graphql

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,13 @@ type IntegrationTriggersConnection {
416416
edges: [IntegrationTriggerEdge!]!
417417
}
418418

419+
type Contact {
420+
id: ID!
421+
createdAt: DateTime!
422+
address: String!
423+
tags: [String!]
424+
}
425+
419426
type AccountCredential {
420427
id: ID!
421428
createdAt: DateTime!
@@ -550,13 +557,6 @@ type UserEdge {
550557
cursor: ConnectionCursor!
551558
}
552559

553-
type Contact {
554-
id: ID!
555-
createdAt: DateTime!
556-
address: String!
557-
tags: [String!]
558-
}
559-
560560
type ContactDeleteResponse {
561561
id: ID
562562
createdAt: DateTime

libs/definitions/src/integration-definition.factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ChatbotDefinition } from './integration-definitions/chatbot/chatbot.def
2121
import { ClvscanDefinition } from './integration-definitions/clvscan.definition'
2222
import { CoinbaseDefinition } from './integration-definitions/coinbase.definition'
2323
import { CoinMarketCapDefinition } from './integration-definitions/coinmarketcap.definition'
24+
import { ContactsDefinition } from './integration-definitions/contacts/contacts.definition'
2425
import { CronoscanDefinition } from './integration-definitions/cronoscan.definition'
2526
import { DecentralandMarketplaceDefinition } from './integration-definitions/decentraland-marketplace.definition'
2627
import { DexScreenerDefinition } from './integration-definitions/dexscreener.definition'
@@ -96,6 +97,7 @@ export class IntegrationDefinitionFactory {
9697
clvscan: ClvscanDefinition,
9798
coinbase: CoinbaseDefinition,
9899
coinmarketcap: CoinMarketCapDefinition,
100+
contacts: ContactsDefinition,
99101
cronoscan: CronoscanDefinition,
100102
// compoundv2: CompoundV2Definition,
101103
'decentraland-marketplace': DecentralandMarketplaceDefinition,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { AuthenticationError } from '@app/common/errors/authentication-error'
2+
import { RunResponse } from '@app/definitions/definition'
3+
import { OperationOffChain } from '@app/definitions/opertion-offchain'
4+
import { ContactService } from 'apps/api/src/contacts/services/contact.service'
5+
import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
6+
import { isAddress } from 'ethers/lib/utils'
7+
import { JSONSchema7, JSONSchema7Definition } from 'json-schema'
8+
import { ObjectId } from 'mongodb'
9+
10+
export class AddTagContactAction extends OperationOffChain {
11+
key = 'addTagContact'
12+
name = 'Add a tag to a contact'
13+
description = 'Add one or more tags to a contact'
14+
version = '1.0.0'
15+
16+
inputs: JSONSchema7 = {
17+
required: ['address', 'tags'],
18+
properties: {
19+
// TODO this should be auto-filled on chatbots but not automations
20+
address: {
21+
title: 'Wallet Address',
22+
type: 'string',
23+
'x-ui:widget': 'hidden',
24+
default: '{{contact.address}}',
25+
} as JSONSchema7Definition,
26+
tags: {
27+
title: 'Tags',
28+
type: 'string',
29+
description: 'Tags to add to the contact. Separate multiple tags with a comma.',
30+
},
31+
},
32+
}
33+
outputs: JSONSchema7 = {
34+
properties: {
35+
tags: {
36+
type: 'array',
37+
items: {
38+
type: 'string',
39+
},
40+
},
41+
},
42+
}
43+
44+
async run({ inputs, user }: OperationRunOptions): Promise<RunResponse> {
45+
if (!isAddress(inputs.address)) {
46+
throw new AuthenticationError('Invalid address')
47+
}
48+
const tags: string[] = inputs?.tags?.split(',').map((tag: string) => tag.trim()) ?? []
49+
const updatedTags = await ContactService.instance.addTags(inputs.address, tags, new ObjectId(user!.id))
50+
return {
51+
outputs: {
52+
tags: updatedTags,
53+
},
54+
}
55+
}
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { AuthenticationError } from '@app/common/errors/authentication-error'
2+
import { RunResponse } from '@app/definitions/definition'
3+
import { OperationOffChain } from '@app/definitions/opertion-offchain'
4+
import { ContactService } from 'apps/api/src/contacts/services/contact.service'
5+
import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
6+
import { isAddress } from 'ethers/lib/utils'
7+
import { JSONSchema7, JSONSchema7Definition } from 'json-schema'
8+
import { ObjectId } from 'mongodb'
9+
10+
export class RemoveTagContactAction extends OperationOffChain {
11+
key = 'removeTagContact'
12+
name = 'Remove a tag from a contact'
13+
description = 'Remove one or more tags from a contact'
14+
version = '1.0.0'
15+
16+
inputs: JSONSchema7 = {
17+
required: ['address', 'tags'],
18+
properties: {
19+
// TODO this should be auto-filled on chatbots but not automations
20+
address: {
21+
title: 'Wallet Address',
22+
type: 'string',
23+
'x-ui:widget': 'hidden',
24+
default: '{{contact.address}}',
25+
} as JSONSchema7Definition,
26+
tags: {
27+
title: 'Tags',
28+
type: 'string',
29+
description: 'Tags to add to the contact. Separate multiple tags with a comma.',
30+
},
31+
},
32+
}
33+
outputs: JSONSchema7 = {
34+
properties: {
35+
tags: {
36+
type: 'array',
37+
items: {
38+
type: 'string',
39+
},
40+
},
41+
},
42+
}
43+
44+
async run({ inputs, user }: OperationRunOptions): Promise<RunResponse> {
45+
if (!isAddress(inputs.address)) {
46+
throw new AuthenticationError('Invalid address')
47+
}
48+
const tags: string[] = inputs?.tags?.split(',').map((tag: string) => tag.trim()) ?? []
49+
const updatedTags = await ContactService.instance.removeTags(inputs.address, tags, new ObjectId(user!.id))
50+
return {
51+
outputs: {
52+
tags: updatedTags,
53+
},
54+
}
55+
}
56+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { SingleIntegrationDefinition } from '@app/definitions/single-integration.definition'
2+
import { AddTagContactAction } from './actions/add-tag-contact.action'
3+
import { RemoveTagContactAction } from './actions/remove-tag-contact.action'
4+
5+
export class ContactsDefinition extends SingleIntegrationDefinition {
6+
integrationKey = 'contacts'
7+
integrationVersion = '1'
8+
schemaUrl = null
9+
10+
triggers = []
11+
actions = [new AddTagContactAction(), new RemoveTagContactAction()]
12+
}

schemas/openapi3/contacts-1.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "Contacts",
5+
"version": "1",
6+
"x-categories": [],
7+
"x-logo": { "url": "/img/contact.svg" }
8+
},
9+
"paths": {}
10+
}

0 commit comments

Comments
 (0)