Skip to content

Commit

Permalink
Merge pull request #483 from xmtp/rygine/pppp-updates
Browse files Browse the repository at this point in the history
`block` => `deny`, update consent on message send if unknown
  • Loading branch information
rygine authored Nov 1, 2023
2 parents 0cdec5e + 0a12b6b commit 731c2cb
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 53 deletions.
67 changes: 50 additions & 17 deletions src/Contacts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import Client from './Client'
import { privatePreferences } from '@xmtp/proto'
import { EnvelopeWithMessage, buildUserPrivatePreferencesTopic } from './utils'
import {
EnvelopeWithMessage,
buildUserPrivatePreferencesTopic,
fromNanoString,
} from './utils'

export type ConsentState = 'allowed' | 'blocked' | 'unknown'
export type ConsentState = 'allowed' | 'denied' | 'unknown'

export type ConsentListEntryType = 'address'

Expand Down Expand Up @@ -36,6 +40,7 @@ export class ConsentListEntry {
export class ConsentList {
client: Client
entries: Map<string, ConsentState>
lastEntryTimestamp?: Date
private _identifier: string | undefined

constructor(client: Client) {
Expand All @@ -49,9 +54,9 @@ export class ConsentList {
return entry
}

block(address: string) {
const entry = ConsentListEntry.fromAddress(address, 'blocked')
this.entries.set(entry.key, 'blocked')
deny(address: string) {
const entry = ConsentListEntry.fromAddress(address, 'denied')
this.entries.set(entry.key, 'denied')
return entry
}

Expand All @@ -78,9 +83,16 @@ export class ConsentList {
const identifier = await this.getIdentifier()
const contentTopic = buildUserPrivatePreferencesTopic(identifier)

let lastTimestampNs: string | undefined

const messages = await this.client.listEnvelopes(
contentTopic,
async ({ message }: EnvelopeWithMessage) => message,
async ({ message, timestampNs }: EnvelopeWithMessage) => {
if (timestampNs) {
lastTimestampNs = timestampNs
}
return message
},
{
startTime,
}
Expand Down Expand Up @@ -108,9 +120,13 @@ export class ConsentList {
this.allow(address)
})
action.block?.walletAddresses.forEach((address) => {
this.block(address)
this.deny(address)
})
})

if (lastTimestampNs) {
this.lastEntryTimestamp = fromNanoString(lastTimestampNs)
}
}

async publish(entries: ConsentListEntry[]) {
Expand All @@ -128,7 +144,7 @@ export class ConsentList {
}
: undefined,
block:
entry.permissionType === 'blocked'
entry.permissionType === 'denied'
? {
walletAddresses: [entry.value],
}
Expand Down Expand Up @@ -181,10 +197,6 @@ export class Contacts {
* XMTP client
*/
client: Client
/**
* The last time the consent list was synced
*/
lastSyncedAt?: Date
private consentList: ConsentList

constructor(client: Client) {
Expand All @@ -194,20 +206,41 @@ export class Contacts {
}

async loadConsentList(startTime?: Date) {
this.lastSyncedAt = new Date()
await this.consentList.load(startTime)
}

async refreshConsentList() {
await this.loadConsentList()
}

/**
* The timestamp of the last entry in the consent list
*/
get lastSyncedAt() {
return this.consentList.lastEntryTimestamp
}

setConsentListEntries(entries: ConsentListEntry[]) {
if (!entries.length) {
return
}
this.consentList.entries.clear()
entries.forEach((entry) => {
if (entry.permissionType === 'allowed') {
this.consentList.allow(entry.value)
}
if (entry.permissionType === 'denied') {
this.consentList.deny(entry.value)
}
})
}

isAllowed(address: string) {
return this.consentList.state(address) === 'allowed'
}

isBlocked(address: string) {
return this.consentList.state(address) === 'blocked'
isDenied(address: string) {
return this.consentList.state(address) === 'denied'
}

consentState(address: string) {
Expand All @@ -222,10 +255,10 @@ export class Contacts {
)
}

async block(addresses: string[]) {
async deny(addresses: string[]) {
await this.consentList.publish(
addresses.map((address) =>
ConsentListEntry.fromAddress(address, 'blocked')
ConsentListEntry.fromAddress(address, 'denied')
)
)
}
Expand Down
38 changes: 26 additions & 12 deletions src/conversations/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ export interface Conversation<ContentTypes = any> {
*/
allow(): Promise<void>
/**
* Add conversation peer address to block list
* Add conversation peer address to deny list
*/
block(): Promise<void>
deny(): Promise<void>

/**
* Returns true if conversation peer address is on the allow list
*/
isAllowed: boolean
/**
* Returns true if conversation peer address is on the block list
* Returns true if conversation peer address is on the deny list
*/
isBlocked: boolean
isDenied: boolean
/**
* Returns the consent state of the conversation peer address
*/
Expand Down Expand Up @@ -191,16 +191,16 @@ export class ConversationV1<ContentTypes>
await this.client.contacts.allow([this.peerAddress])
}

async block() {
await this.client.contacts.block([this.peerAddress])
async deny() {
await this.client.contacts.deny([this.peerAddress])
}

get isAllowed() {
return this.client.contacts.isAllowed(this.peerAddress)
}

get isBlocked() {
return this.client.contacts.isBlocked(this.peerAddress)
get isDenied() {
return this.client.contacts.isDenied(this.peerAddress)
}

get consentState() {
Expand Down Expand Up @@ -410,6 +410,13 @@ export class ConversationV1<ContentTypes>
}))
)

// if the conversation consent state is unknown, we assume the user has
// consented to the conversation by sending a message into it
if (this.consentState === 'unknown') {
// add conversation to the allow list
await this.allow()
}

return DecodedMessage.fromV1Message(
msg,
content,
Expand Down Expand Up @@ -522,16 +529,16 @@ export class ConversationV2<ContentTypes>
await this.client.contacts.allow([this.peerAddress])
}

async block() {
await this.client.contacts.block([this.peerAddress])
async deny() {
await this.client.contacts.deny([this.peerAddress])
}

get isAllowed() {
return this.client.contacts.isAllowed(this.peerAddress)
}

get isBlocked() {
return this.client.contacts.isBlocked(this.peerAddress)
get isDenied() {
return this.client.contacts.isDenied(this.peerAddress)
}

get consentState() {
Expand Down Expand Up @@ -615,6 +622,13 @@ export class ConversationV2<ContentTypes>
])
const contentType = options?.contentType || ContentTypeText

// if the conversation consent state is unknown, we assume the user has
// consented to the conversation by sending a message into it
if (this.consentState === 'unknown') {
// add conversation to the allow list
await this.allow()
}

return DecodedMessage.fromV2Message(
msg,
content,
Expand Down
69 changes: 45 additions & 24 deletions test/Contacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ describe('Contacts', () => {
expect(Array.from(aliceClient.contacts.addresses.keys()).length).toBe(0)
})

it('should allow and block addresses', async () => {
it('should allow and deny addresses', async () => {
await aliceClient.contacts.allow([bob.address])
expect(aliceClient.contacts.consentState(bob.address)).toBe('allowed')
expect(aliceClient.contacts.isAllowed(bob.address)).toBe(true)
expect(aliceClient.contacts.isBlocked(bob.address)).toBe(false)
expect(aliceClient.contacts.isDenied(bob.address)).toBe(false)

await aliceClient.contacts.block([bob.address])
expect(aliceClient.contacts.consentState(bob.address)).toBe('blocked')
await aliceClient.contacts.deny([bob.address])
expect(aliceClient.contacts.consentState(bob.address)).toBe('denied')
expect(aliceClient.contacts.isAllowed(bob.address)).toBe(false)
expect(aliceClient.contacts.isBlocked(bob.address)).toBe(true)
expect(aliceClient.contacts.isDenied(bob.address)).toBe(true)
})

it('should allow an address when a conversation is started', async () => {
Expand All @@ -48,54 +48,75 @@ describe('Contacts', () => {

expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed')
expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true)
expect(aliceClient.contacts.isBlocked(carol.address)).toBe(false)
expect(aliceClient.contacts.isDenied(carol.address)).toBe(false)

expect(conversation.isAllowed).toBe(true)
expect(conversation.isBlocked).toBe(false)
expect(conversation.isDenied).toBe(false)
expect(conversation.consentState).toBe('allowed')
})

it('should allow or block an address from a conversation', async () => {
it('should allow an address when a conversation has an unknown consent state and a message is sent into it', async () => {
await aliceClient.conversations.newConversation(carol.address)

expect(carolClient.contacts.consentState(alice.address)).toBe('unknown')
expect(carolClient.contacts.isAllowed(carol.address)).toBe(false)
expect(carolClient.contacts.isDenied(carol.address)).toBe(false)

const carolConversation = await carolClient.conversations.newConversation(
alice.address
)
expect(carolConversation.consentState).toBe('unknown')
expect(carolConversation.isAllowed).toBe(false)
expect(carolConversation.isDenied).toBe(false)

await carolConversation.send('gm')

expect(carolConversation.consentState).toBe('allowed')
expect(carolConversation.isAllowed).toBe(true)
expect(carolConversation.isDenied).toBe(false)
})

it('should allow or deny an address from a conversation', async () => {
const conversation = await aliceClient.conversations.newConversation(
carol.address
)

await conversation.block()
await conversation.deny()

expect(aliceClient.contacts.consentState(carol.address)).toBe('blocked')
expect(aliceClient.contacts.consentState(carol.address)).toBe('denied')
expect(aliceClient.contacts.isAllowed(carol.address)).toBe(false)
expect(aliceClient.contacts.isBlocked(carol.address)).toBe(true)
expect(aliceClient.contacts.isDenied(carol.address)).toBe(true)

expect(conversation.isAllowed).toBe(false)
expect(conversation.isBlocked).toBe(true)
expect(conversation.consentState).toBe('blocked')
expect(conversation.isDenied).toBe(true)
expect(conversation.consentState).toBe('denied')

await conversation.allow()

expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed')
expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true)
expect(aliceClient.contacts.isBlocked(carol.address)).toBe(false)
expect(aliceClient.contacts.isDenied(carol.address)).toBe(false)

expect(conversation.isAllowed).toBe(true)
expect(conversation.isBlocked).toBe(false)
expect(conversation.isDenied).toBe(false)
expect(conversation.consentState).toBe('allowed')
})

it('should retrieve consent state', async () => {
await aliceClient.contacts.block([bob.address])
await aliceClient.contacts.deny([bob.address])
await aliceClient.contacts.allow([carol.address])
await aliceClient.contacts.allow([bob.address])
await aliceClient.contacts.block([carol.address])
await aliceClient.contacts.block([bob.address])
await aliceClient.contacts.deny([carol.address])
await aliceClient.contacts.deny([bob.address])
await aliceClient.contacts.allow([carol.address])

expect(aliceClient.contacts.consentState(bob.address)).toBe('blocked')
expect(aliceClient.contacts.consentState(bob.address)).toBe('denied')
expect(aliceClient.contacts.isAllowed(bob.address)).toBe(false)
expect(aliceClient.contacts.isBlocked(bob.address)).toBe(true)
expect(aliceClient.contacts.isDenied(bob.address)).toBe(true)

expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed')
expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true)
expect(aliceClient.contacts.isBlocked(carol.address)).toBe(false)
expect(aliceClient.contacts.isDenied(carol.address)).toBe(false)

aliceClient = await Client.create(alice, {
env: 'local',
Expand All @@ -106,12 +127,12 @@ describe('Contacts', () => {

await aliceClient.contacts.refreshConsentList()

expect(aliceClient.contacts.consentState(bob.address)).toBe('blocked')
expect(aliceClient.contacts.consentState(bob.address)).toBe('denied')
expect(aliceClient.contacts.isAllowed(bob.address)).toBe(false)
expect(aliceClient.contacts.isBlocked(bob.address)).toBe(true)
expect(aliceClient.contacts.isDenied(bob.address)).toBe(true)

expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed')
expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true)
expect(aliceClient.contacts.isBlocked(carol.address)).toBe(false)
expect(aliceClient.contacts.isDenied(carol.address)).toBe(false)
})
})

0 comments on commit 731c2cb

Please sign in to comment.