Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions migrations/20260410_230000_add_is_vanished_to_users_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
exports.up = async function (knex) {
await knex.schema.alterTable('users', (table) => {
table.boolean('is_vanished').notNullable().defaultTo(false)
})

await knex.raw(`
UPDATE users u
SET is_vanished = true
WHERE EXISTS (
SELECT 1 FROM events e
WHERE e.event_pubkey = u.pubkey
AND e.event_kind = 62
AND e.deleted_at IS NULL
)
`)
}

exports.down = function (knex) {
return knex.schema.alterTable('users', (table) => {
table.dropColumn('is_vanished')
})
}
2 changes: 2 additions & 0 deletions src/@types/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ export interface IUserRepository {
findByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise<User | undefined>
upsert(user: Partial<User>, client?: DatabaseClient): Promise<number>
getBalanceByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise<bigint>
isVanished(pubkey: Pubkey, client?: DatabaseClient): Promise<boolean>
setVanished(pubkey: Pubkey, vanished: boolean, client?: DatabaseClient): Promise<number>
}
2 changes: 2 additions & 0 deletions src/@types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Pubkey } from './base'
export interface User {
pubkey: Pubkey
isAdmitted: boolean
isVanished: boolean
balance: bigint
tosAcceptedAt?: Date | null
createdAt: Date
Expand All @@ -12,6 +13,7 @@ export interface User {
export interface DBUser {
pubkey: Buffer
is_admitted: boolean
is_vanished: boolean
balance: bigint
created_at: Date
updated_at: Date
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { getMasterDbClient, getReadReplicaDbClient } from '../../database/client'
import { createSettings } from '../settings-factory'
import { getMasterDbClient } from '../../database/client'
import { EventRepository } from '../../repositories/event-repository'
import { GetSubmissionCheckController } from '../../controllers/admission/get-admission-check-controller'
import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory'
import { UserRepository } from '../../repositories/user-repository'

export const createGetAdmissionCheckController = () => {
const dbClient = getMasterDbClient()
const userRepository = new UserRepository(dbClient)
const readReplicaDbClient = getReadReplicaDbClient()
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
const userRepository = new UserRepository(dbClient, eventRepository)

return new GetSubmissionCheckController(
userRepository,
Expand Down
7 changes: 5 additions & 2 deletions src/factories/controllers/post-invoice-controller-factory.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { getMasterDbClient, getReadReplicaDbClient } from '../../database/client'
import { createPaymentsService } from '../payments-service-factory'
import { createSettings } from '../settings-factory'
import { getMasterDbClient } from '../../database/client'
import { EventRepository } from '../../repositories/event-repository'
import { IController } from '../../@types/controllers'
import { PostInvoiceController } from '../../controllers/invoices/post-invoice-controller'
import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory'
import { UserRepository } from '../../repositories/user-repository'

export const createPostInvoiceController = (): IController => {
const dbClient = getMasterDbClient()
const userRepository = new UserRepository(dbClient)
const readReplicaDbClient = getReadReplicaDbClient()
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
const userRepository = new UserRepository(dbClient, eventRepository)
const paymentsService = createPaymentsService()

return new PostInvoiceController(
Expand Down
5 changes: 3 additions & 2 deletions src/factories/event-strategy-factory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IEventRepository, IUserRepository } from '../@types/repositories'
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } from '../utils/event'
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
import { Event } from '../@types/event'
import { Factory } from '../@types/base'
import { IEventRepository } from '../@types/repositories'
import { IEventStrategy } from '../@types/message-handlers'
import { IWebSocketAdapter } from '../@types/adapters'
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
Expand All @@ -13,10 +13,11 @@ import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-s

export const eventStrategyFactory = (
eventRepository: IEventRepository,
userRepository: IUserRepository,
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
([event, adapter]: [Event, IWebSocketAdapter]) => {
if (isRequestToVanishEvent(event)) {
return new VanishEventStrategy(adapter, eventRepository)
return new VanishEventStrategy(adapter, eventRepository, userRepository)
} else if (isReplaceableEvent(event)) {
return new ReplaceableEventStrategy(adapter, eventRepository)
} else if (isEphemeralEvent(event)) {
Expand Down
2 changes: 1 addition & 1 deletion src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const messageHandlerFactory = (
{
return new EventMessageHandler(
adapter,
eventStrategyFactory(eventRepository),
eventStrategyFactory(eventRepository, userRepository),
eventRepository,
userRepository,
createSettings,
Expand Down
4 changes: 2 additions & 2 deletions src/factories/payments-service-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export const createPaymentsService = () => {
const dbClient = getMasterDbClient()
const rrDbClient = getReadReplicaDbClient()
const invoiceRepository = new InvoiceRepository(dbClient)
const userRepository = new UserRepository(dbClient)
const paymentsProcessor = createPaymentsProcessor()
const eventRepository = new EventRepository(dbClient, rrDbClient)
const userRepository = new UserRepository(dbClient, eventRepository)
const paymentsProcessor = createPaymentsProcessor()

return new PaymentsService(
dbClient,
Expand Down
2 changes: 1 addition & 1 deletion src/factories/static-mirroring.worker-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const staticMirroringWorkerFactory = () => {
const dbClient = getMasterDbClient()
const readReplicaDbClient = getReadReplicaDbClient()
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
const userRepository = new UserRepository(dbClient)
const userRepository = new UserRepository(dbClient, eventRepository)

return new StaticMirroringWorker(
eventRepository,
Expand Down
2 changes: 1 addition & 1 deletion src/factories/worker-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const workerFactory = (): AppWorker => {
const dbClient = getMasterDbClient()
const readReplicaDbClient = getReadReplicaDbClient()
const eventRepository = new EventRepository(dbClient, readReplicaDbClient)
const userRepository = new UserRepository(dbClient)
const userRepository = new UserRepository(dbClient, eventRepository)

const settings = createSettings()

Expand Down
4 changes: 2 additions & 2 deletions src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ export class EventMessageHandler implements IMessageHandler {
return
}

const existingVanishRequest = await this.eventRepository.hasActiveRequestToVanish(event.pubkey)
if (existingVanishRequest) {
const isVanished = await this.userRepository.isVanished(event.pubkey)
if (isVanished) {
return 'blocked: request to vanish active for pubkey'
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/handlers/event-strategies/vanish-event-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { IEventRepository, IUserRepository } from '../../@types/repositories'
import { createCommandResult } from '../../utils/messages'
import { createLogger } from '../../factories/logger-factory'
import { Event } from '../../@types/event'
import { EventKinds } from '../../constants/base'
import { IEventRepository } from '../../@types/repositories'
import { IEventStrategy } from '../../@types/message-handlers'
import { IWebSocketAdapter } from '../../@types/adapters'
import { WebSocketAdapterEvent } from '../../constants/adapter'
Expand All @@ -13,6 +13,7 @@ export class VanishEventStrategy implements IEventStrategy<Event, Promise<void>>
public constructor(
private readonly webSocket: IWebSocketAdapter,
private readonly eventRepository: IEventRepository,
private readonly userRepository: IUserRepository,
) {}

public async execute(event: Event): Promise<void> {
Expand All @@ -25,6 +26,8 @@ export class VanishEventStrategy implements IEventStrategy<Event, Promise<void>>

const count = await this.eventRepository.create(event)

await this.userRepository.setVanished(event.pubkey, true)

this.webSocket.emit(
WebSocketAdapterEvent.Message,
createCommandResult(event.id, true, count ? '' : 'duplicate:')
Expand Down
72 changes: 66 additions & 6 deletions src/repositories/user-repository.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { always, applySpec, omit, pipe, prop } from 'ramda'

import { always, applySpec, defaultTo, omit, pipe, prop } from 'ramda'
import { DatabaseClient, Pubkey } from '../@types/base'
import { DBUser, User } from '../@types/user'
import { fromDBUser, toBuffer } from '../utils/transform'
import { IEventRepository, IUserRepository } from '../@types/repositories'
import { createLogger } from '../factories/logger-factory'
import { IUserRepository } from '../@types/repositories'


const debug = createLogger('user-repository')

export class UserRepository implements IUserRepository {
public constructor(private readonly dbClient: DatabaseClient) { }
public constructor(
private readonly dbClient: DatabaseClient,
private readonly eventRepository: IEventRepository,
) { }

public async findByPubkey(
pubkey: Pubkey,
Expand All @@ -28,7 +31,7 @@ export class UserRepository implements IUserRepository {
}

public async upsert(
user: User,
user: Partial<User>,
client: DatabaseClient = this.dbClient,
): Promise<number> {
debug('upsert: %o', user)
Expand All @@ -37,7 +40,8 @@ export class UserRepository implements IUserRepository {

const row = applySpec<DBUser>({
pubkey: pipe(prop('pubkey'), toBuffer),
is_admitted: prop('isAdmitted'),
is_admitted: pipe(prop('isAdmitted'), defaultTo(false)),
is_vanished: pipe(prop('isVanished'), defaultTo(false)),
tos_accepted_at: prop('tosAcceptedAt'),
updated_at: always(date),
created_at: always(date),
Expand All @@ -61,6 +65,62 @@ export class UserRepository implements IUserRepository {
} as Promise<number>
}

/**
* Returns vanish state from users.is_vanished, or lazily hydrates a user row from events once
* when no users row exists (single upsert; no duplicate inserts).
*/
public async isVanished(
pubkey: Pubkey,
client: DatabaseClient = this.dbClient
): Promise<boolean> {
const existing = await this.findByPubkey(pubkey, client)
if (existing) {
return existing.isVanished
}

const vanishedFromEvents = await this.eventRepository.hasActiveRequestToVanish(pubkey)
await this.upsertVanishState(pubkey, vanishedFromEvents, client)
return vanishedFromEvents
}

public setVanished(
pubkey: Pubkey,
vanished: boolean,
client: DatabaseClient = this.dbClient
): Promise<number> {
return this.upsertVanishState(pubkey, vanished, client)
}

private upsertVanishState(
pubkey: Pubkey,
isVanished: boolean,
client: DatabaseClient,
): Promise<number> {
debug('upsert vanish state for %s: %o', pubkey, isVanished)
const date = new Date()

const query = client<DBUser>('users')
.insert({
pubkey: toBuffer(pubkey),
is_admitted: false,
balance: 0n,
is_vanished: isVanished,
created_at: date,
updated_at: date,
})
.onConflict('pubkey')
.merge({
is_vanished: isVanished,
updated_at: date,
})

return {
then: <T1, T2>(onfulfilled: (value: number) => T1 | PromiseLike<T1>, onrejected: (reason: any) => T2 | PromiseLike<T2>) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected),
catch: <T>(onrejected: (reason: any) => T | PromiseLike<T>) => query.catch(onrejected),
toString: (): string => query.toString(),
} as Promise<number>
}

public async getBalanceByPubkey(
pubkey: Pubkey,
client: DatabaseClient = this.dbClient
Expand Down
1 change: 1 addition & 0 deletions src/utils/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const fromDBInvoice = applySpec<Invoice>({
export const fromDBUser = applySpec<User>({
pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer),
isAdmitted: prop('is_admitted'),
isVanished: prop('is_vanished'),
balance: prop('balance'),
createdAt: prop('created_at'),
updatedAt: prop('updated_at'),
Expand Down
6 changes: 4 additions & 2 deletions test/unit/factories/event-strategy-factory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expect } from 'chai'

import { IEventRepository, IUserRepository } from '../../../src/@types/repositories'
import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy'
import { DeleteEventStrategy } from '../../../src/handlers/event-strategies/delete-event-strategy'
import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy'
import { Event } from '../../../src/@types/event'
import { EventKinds } from '../../../src/constants/base'
import { eventStrategyFactory } from '../../../src/factories/event-strategy-factory'
import { Factory } from '../../../src/@types/base'
import { IEventRepository } from '../../../src/@types/repositories'
import { IEventStrategy } from '../../../src/@types/message-handlers'
import { IWebSocketAdapter } from '../../../src/@types/adapters'
import { ParameterizedReplaceableEventStrategy } from '../../../src/handlers/event-strategies/parameterized-replaceable-event-strategy'
Expand All @@ -16,16 +16,18 @@ import { VanishEventStrategy } from '../../../src/handlers/event-strategies/vani

describe('eventStrategyFactory', () => {
let eventRepository: IEventRepository
let userRepository: IUserRepository
let event: Event
let adapter: IWebSocketAdapter
let factory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>

beforeEach(() => {
eventRepository = {} as any
userRepository = {} as any
event = {} as any
adapter = {} as any

factory = eventStrategyFactory(eventRepository)
factory = eventStrategyFactory(eventRepository, userRepository)
})

it('returns ReplaceableEvent given a set_metadata event', () => {
Expand Down
Loading