From 39e2956f131423129663cb5e7adf18a45f09b571 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 24 Jan 2024 09:03:18 -0600 Subject: [PATCH 01/10] Entryway: tagged suggestions (#2078) * proxy getTaggedSuggestions on entryway * rm unused err --- .../bsky/unspecced/getTaggedSuggestions.json | 42 ++++++++++++ packages/api/src/client/index.ts | 13 ++++ packages/api/src/client/lexicons.ts | 50 ++++++++++++++ .../bsky/unspecced/getTaggedSuggestions.ts | 55 ++++++++++++++++ packages/bsky/src/lexicon/index.ts | 14 +++- packages/bsky/src/lexicon/lexicons.ts | 50 ++++++++++++++ .../bsky/unspecced/getTaggedSuggestions.ts | 65 +++++++++++++++++++ .../bsky/unspecced/getTaggedSuggestions.ts | 44 +++++++++++++ .../pds/src/api/app/bsky/unspecced/index.ts | 2 + packages/pds/src/lexicon/index.ts | 14 +++- packages/pds/src/lexicon/lexicons.ts | 50 ++++++++++++++ .../bsky/unspecced/getTaggedSuggestions.ts | 65 +++++++++++++++++++ 12 files changed, 460 insertions(+), 4 deletions(-) create mode 100644 lexicons/app/bsky/unspecced/getTaggedSuggestions.json create mode 100644 packages/api/src/client/types/app/bsky/unspecced/getTaggedSuggestions.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts create mode 100644 packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts diff --git a/lexicons/app/bsky/unspecced/getTaggedSuggestions.json b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json new file mode 100644 index 00000000000..9fd98ffefb0 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTaggedSuggestions", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggestions (feeds and users) tagged with categories", + "parameters": { + "type": "params", + "properties": {} + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["suggestions"], + "properties": { + "suggestions": { + "type": "array", + "items": { + "type": "ref", + "ref": "#suggestion" + } + } + } + } + } + }, + "suggestion": { + "type": "object", + "required": ["tag", "subjectType", "subject"], + "properties": { + "tag": { "type": "string" }, + "subjectType": { + "type": "string", + "knownValues": ["actor", "feed"] + }, + "subject": { "type": "string", "format": "uri" } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index c996a63b4ea..2ff30e38151 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -147,6 +147,7 @@ import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' import * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' +import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' @@ -291,6 +292,7 @@ export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' export * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' export * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' export * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' +export * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' export * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' export * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' export * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' @@ -2508,6 +2510,17 @@ export class UnspeccedNS { }) } + getTaggedSuggestions( + params?: AppBskyUnspeccedGetTaggedSuggestions.QueryParams, + opts?: AppBskyUnspeccedGetTaggedSuggestions.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.unspecced.getTaggedSuggestions', params, undefined, opts) + .catch((e) => { + throw AppBskyUnspeccedGetTaggedSuggestions.toKnownErr(e) + }) + } + getTimelineSkeleton( params?: AppBskyUnspeccedGetTimelineSkeleton.QueryParams, opts?: AppBskyUnspeccedGetTimelineSkeleton.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 09a586338ea..86aaa2e8e19 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -7924,6 +7924,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTaggedSuggestions: { + lexicon: 1, + id: 'app.bsky.unspecced.getTaggedSuggestions', + defs: { + main: { + type: 'query', + description: + 'Get a list of suggestions (feeds and users) tagged with categories', + parameters: { + type: 'params', + properties: {}, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['suggestions'], + properties: { + suggestions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion', + }, + }, + }, + }, + }, + }, + suggestion: { + type: 'object', + required: ['tag', 'subjectType', 'subject'], + properties: { + tag: { + type: 'string', + }, + subjectType: { + type: 'string', + knownValues: ['actor', 'feed'], + }, + subject: { + type: 'string', + format: 'uri', + }, + }, + }, + }, + }, AppBskyUnspeccedGetTimelineSkeleton: { lexicon: 1, id: 'app.bsky.unspecced.getTimelineSkeleton', @@ -8257,6 +8305,8 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTaggedSuggestions: + 'app.bsky.unspecced.getTaggedSuggestions', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', AppBskyUnspeccedSearchActorsSkeleton: 'app.bsky.unspecced.searchActorsSkeleton', diff --git a/packages/api/src/client/types/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/api/src/client/types/app/bsky/unspecced/getTaggedSuggestions.ts new file mode 100644 index 00000000000..a35e2411756 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/getTaggedSuggestions.ts @@ -0,0 +1,55 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + suggestions: Suggestion[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} + +export interface Suggestion { + tag: string + subjectType: 'actor' | 'feed' | (string & {}) + subject: string + [k: string]: unknown +} + +export function isSuggestion(v: unknown): v is Suggestion { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.getTaggedSuggestions#suggestion' + ) +} + +export function validateSuggestion(v: unknown): ValidationResult { + return lexicons.validate( + 'app.bsky.unspecced.getTaggedSuggestions#suggestion', + v, + ) +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 917078e5bb0..4efd7f7837a 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -124,6 +124,7 @@ import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/ import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' +import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' @@ -1613,6 +1614,17 @@ export class UnspeccedNS { return this._server.xrpc.method(nsid, cfg) } + getTaggedSuggestions( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetTaggedSuggestions.Handler>, + AppBskyUnspeccedGetTaggedSuggestions.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getTimelineSkeleton( cfg: ConfigOf< AV, @@ -1658,13 +1670,11 @@ type RouteRateLimitOpts = { calcKey?: (ctx: T) => string calcPoints?: (ctx: T) => number } -type HandlerOpts = { blobLimit?: number } type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts type ConfigOf = | Handler | { auth?: Auth - opts?: HandlerOpts rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 09a586338ea..86aaa2e8e19 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -7924,6 +7924,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTaggedSuggestions: { + lexicon: 1, + id: 'app.bsky.unspecced.getTaggedSuggestions', + defs: { + main: { + type: 'query', + description: + 'Get a list of suggestions (feeds and users) tagged with categories', + parameters: { + type: 'params', + properties: {}, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['suggestions'], + properties: { + suggestions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion', + }, + }, + }, + }, + }, + }, + suggestion: { + type: 'object', + required: ['tag', 'subjectType', 'subject'], + properties: { + tag: { + type: 'string', + }, + subjectType: { + type: 'string', + knownValues: ['actor', 'feed'], + }, + subject: { + type: 'string', + format: 'uri', + }, + }, + }, + }, + }, AppBskyUnspeccedGetTimelineSkeleton: { lexicon: 1, id: 'app.bsky.unspecced.getTimelineSkeleton', @@ -8257,6 +8305,8 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTaggedSuggestions: + 'app.bsky.unspecced.getTaggedSuggestions', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', AppBskyUnspeccedSearchActorsSkeleton: 'app.bsky.unspecced.searchActorsSkeleton', diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts new file mode 100644 index 00000000000..e6319c54b4e --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts @@ -0,0 +1,65 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + suggestions: Suggestion[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput + +export interface Suggestion { + tag: string + subjectType: 'actor' | 'feed' | (string & {}) + subject: string + [k: string]: unknown +} + +export function isSuggestion(v: unknown): v is Suggestion { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.getTaggedSuggestions#suggestion' + ) +} + +export function validateSuggestion(v: unknown): ValidationResult { + return lexicons.validate( + 'app.bsky.unspecced.getTaggedSuggestions#suggestion', + v, + ) +} diff --git a/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts new file mode 100644 index 00000000000..ff4050bbc5c --- /dev/null +++ b/packages/pds/src/api/app/bsky/unspecced/getTaggedSuggestions.ts @@ -0,0 +1,44 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { + authPassthru, + proxy, + proxyAppView, + resultPassthru, +} from '../../../proxy' + +// THIS IS A TEMPORARY UNSPECCED ROUTE +export default function (server: Server, ctx: AppContext) { + server.app.bsky.unspecced.getTaggedSuggestions({ + auth: ctx.authVerifier.access, + handler: async ({ auth, params, req }) => { + const proxied = await proxy( + ctx, + auth.credentials.audience, + async (agent) => { + const result = + await agent.api.app.bsky.unspecced.getTaggedSuggestions( + params, + authPassthru(req), + ) + return resultPassthru(result) + }, + ) + if (proxied !== null) { + return proxied + } + + const requester = auth.credentials.did + const res = await proxyAppView(ctx, async (agent) => + agent.api.app.bsky.unspecced.getTaggedSuggestions( + params, + await ctx.appviewAuthHeaders(requester), + ), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/unspecced/index.ts b/packages/pds/src/api/app/bsky/unspecced/index.ts index 6951400863d..7f404c76d48 100644 --- a/packages/pds/src/api/app/bsky/unspecced/index.ts +++ b/packages/pds/src/api/app/bsky/unspecced/index.ts @@ -2,9 +2,11 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import getPopular from './getPopular' import getPopularFeedGenerators from './getPopularFeedGenerators' +import getTaggedSuggestions from './getTaggedSuggestions' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { getPopular(server, ctx) getPopularFeedGenerators(server, ctx) + getTaggedSuggestions(server, ctx) } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 917078e5bb0..4efd7f7837a 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -124,6 +124,7 @@ import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/ import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' +import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' @@ -1613,6 +1614,17 @@ export class UnspeccedNS { return this._server.xrpc.method(nsid, cfg) } + getTaggedSuggestions( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetTaggedSuggestions.Handler>, + AppBskyUnspeccedGetTaggedSuggestions.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getTimelineSkeleton( cfg: ConfigOf< AV, @@ -1658,13 +1670,11 @@ type RouteRateLimitOpts = { calcKey?: (ctx: T) => string calcPoints?: (ctx: T) => number } -type HandlerOpts = { blobLimit?: number } type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts type ConfigOf = | Handler | { auth?: Auth - opts?: HandlerOpts rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 09a586338ea..86aaa2e8e19 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -7924,6 +7924,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTaggedSuggestions: { + lexicon: 1, + id: 'app.bsky.unspecced.getTaggedSuggestions', + defs: { + main: { + type: 'query', + description: + 'Get a list of suggestions (feeds and users) tagged with categories', + parameters: { + type: 'params', + properties: {}, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['suggestions'], + properties: { + suggestions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion', + }, + }, + }, + }, + }, + }, + suggestion: { + type: 'object', + required: ['tag', 'subjectType', 'subject'], + properties: { + tag: { + type: 'string', + }, + subjectType: { + type: 'string', + knownValues: ['actor', 'feed'], + }, + subject: { + type: 'string', + format: 'uri', + }, + }, + }, + }, + }, AppBskyUnspeccedGetTimelineSkeleton: { lexicon: 1, id: 'app.bsky.unspecced.getTimelineSkeleton', @@ -8257,6 +8305,8 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTaggedSuggestions: + 'app.bsky.unspecced.getTaggedSuggestions', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', AppBskyUnspeccedSearchActorsSkeleton: 'app.bsky.unspecced.searchActorsSkeleton', diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts new file mode 100644 index 00000000000..e6319c54b4e --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts @@ -0,0 +1,65 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + suggestions: Suggestion[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput + +export interface Suggestion { + tag: string + subjectType: 'actor' | 'feed' | (string & {}) + subject: string + [k: string]: unknown +} + +export function isSuggestion(v: unknown): v is Suggestion { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.getTaggedSuggestions#suggestion' + ) +} + +export function validateSuggestion(v: unknown): ValidationResult { + return lexicons.validate( + 'app.bsky.unspecced.getTaggedSuggestions#suggestion', + v, + ) +} From a3cf3a3adb03f6fd8c074147a5e508a817a4a983 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 12:46:14 -0600 Subject: [PATCH 02/10] update error language --- packages/pds/src/api/com/atproto/server/createAccount.ts | 4 ++-- packages/pds/tests/phone-verification.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 5786ff92868..1d94f876328 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -44,12 +44,12 @@ export default function (server: Server, ctx: AppContext) { if (ctx.cfg.phoneVerification.required && ctx.twilio) { if (!input.body.verificationPhone) { throw new InvalidRequestError( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, 'InvalidPhoneVerification', ) } else if (!input.body.verificationCode) { throw new InvalidRequestError( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, 'InvalidPhoneVerification', ) } diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts index 525845309fb..a203a07ccf9 100644 --- a/packages/pds/tests/phone-verification.test.ts +++ b/packages/pds/tests/phone-verification.test.ts @@ -116,7 +116,7 @@ describe('phone verification', () => { it('does not allow signup with out a code', async () => { const attempt = createAccountWithCode() await expect(attempt).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) }) @@ -125,11 +125,11 @@ describe('phone verification', () => { const bobCode = await requestCode(bobNumber) const attempt = createAccountWithCode(undefined, bobCode) await expect(attempt).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) const attempt2 = createAccountWithCode(bobNumber, undefined) await expect(attempt2).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) }) From b18b766182b2d942b238dc9e85c5022d74d593f8 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:10:34 -0600 Subject: [PATCH 03/10] bugfix twilio --- .../api/com/atproto/server/createAccount.ts | 34 ++++++++++++++----- packages/pds/src/logger.ts | 1 + packages/pds/src/twilio.ts | 7 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 1d94f876328..b78b358fb4c 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -37,6 +37,8 @@ export default function (server: Server, ctx: AppContext) { ? await validateInputsForPdsViaEntryway(ctx, input.body) : await validateInputsForPdsViaUser(ctx, input.body) + await ensureUnusedHandleAndEmail(ctx.db, handle, email) + const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) @@ -72,13 +74,6 @@ export default function (server: Server, ctx: AppContext) { const actorTxn = ctx.services.account(dbTxn) const repoTxn = ctx.services.repo(dbTxn) - // it's a bit goofy that we run this logic twice, - // but we run it once for a sanity check before doing scrypt & plc ops - // & a second time for locking + integrity check - if (ctx.cfg.invites.required && inviteCode) { - await ensureCodeIsAvailable(dbTxn, inviteCode, true) - } - // Register user before going out to PLC to get a real did try { await actorTxn.registerUser({ @@ -238,7 +233,8 @@ const validateInputsForPdsViaUser = async ( ctx: AppContext, input: CreateAccountInput, ) => { - const { email, password, inviteCode } = input + const { password, inviteCode } = input + const email = input.email?.toLowerCase() if (input.plcOp) { throw new InvalidRequestError('Unsupported input: "plcOp"') } @@ -269,6 +265,8 @@ const validateInputsForPdsViaUser = async ( did: input.did, }) + await ensureUnusedHandleAndEmail(ctx.db, handle, email) + // check that the invite code still has uses if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(ctx.db, inviteCode) @@ -470,6 +468,26 @@ const reserveSigningKey = async (ctx: AppContext, host: string) => { } } +const ensureUnusedHandleAndEmail = async ( + db: Database, + handle: string, + email: string, +) => { + const res = await db.db + .selectFrom('user_account') + .innerJoin('did_handle', 'did_handle.did', 'user_account.did') + .select(['did_handle.handle', 'user_account.email']) + .where('user_account.email', '=', email) + .where('did_handle.handle', '=', handle) + .executeTakeFirst() + if (!res) return + if (res.email === email) { + throw new InvalidRequestError(`Email already taken: ${email}`) + } else { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } +} + const randomIndexByWeight = (weights) => { let sum = 0 const cumulative = weights.map((weight) => { diff --git a/packages/pds/src/logger.ts b/packages/pds/src/logger.ts index e8b663b567f..5e93f840fdd 100644 --- a/packages/pds/src/logger.ts +++ b/packages/pds/src/logger.ts @@ -11,6 +11,7 @@ export const seqLogger = subsystemLogger('pds:sequencer') export const mailerLogger = subsystemLogger('pds:mailer') export const labelerLogger = subsystemLogger('pds:labler') export const crawlerLogger = subsystemLogger('pds:crawler') +export const twilioLogger = subsystemLogger('pds:twilio') export const httpLogger = subsystemLogger('pds') export const loggerMiddleware = pinoHttp({ diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index a2d0846e396..113c9e18245 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -1,5 +1,6 @@ import { InvalidRequestError, UpstreamFailureError } from '@atproto/xrpc-server' import twilio from 'twilio' +import { twilioLogger as log } from './logger' type Opts = { accountSid: string @@ -20,7 +21,7 @@ export class TwilioClient { } normalizePhoneNumber(phoneNumber: string) { - let normalized = phoneNumber.replaceAll(/\(|\)|-| /g, '') + let normalized = phoneNumber.trim().replaceAll(/\(|\)|-| /g, '') if (!normalized.startsWith('+')) { if (normalized.length === 10) { normalized = '+1' + normalized @@ -43,6 +44,7 @@ export class TwilioClient { channel: 'sms', }) } catch (err) { + log.error({ err, phoneNumber }, 'error sending twilio code') throw new UpstreamFailureError('Could not send verification text') } } @@ -55,7 +57,8 @@ export class TwilioClient { }) return res.status === 'approved' } catch (err) { - throw new UpstreamFailureError('Could not send verification text') + log.error({ err, phoneNumber, code }, 'error verifying twilio code') + throw new UpstreamFailureError('Could not verify code. Please try again') } } } From f9e51aa418301afaf5f7e1de13b3189393893175 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:19:57 -0600 Subject: [PATCH 04/10] patch up some tests --- packages/pds/tests/account.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index f157380a1c1..c6e127f7162 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -169,7 +169,7 @@ describe('account', () => { const userKey = await crypto.Secp256k1Keypair.create() const baseDidInfo = { signingKey: ctx.repoSigningKey.did(), - handle: 'byo-did.test', + handle: 'byo-did2.test', rotationKeys: [ userKey.did(), ctx.cfg.identity.recoveryDidKey ?? '', @@ -179,9 +179,9 @@ describe('account', () => { signer: userKey, } const baseAccntInfo = { - email: 'byo-did@test.com', - handle: 'byo-did.test', - password: 'byo-did-pass', + email: 'byo-did2@test.com', + handle: 'byo-did2.test', + password: 'byo-did2-pass', } const did1 = await ctx.plcClient.createDid({ @@ -304,7 +304,7 @@ describe('account', () => { handle: 'carol.test', password, }), - ).rejects.toThrow('Email already taken: BOB@TEST.COM') + ).rejects.toThrow('Email already taken: bob@test.com') await expect( agent.api.com.atproto.server.createAccount({ From c7ba6778f8634b7d1470da9050438b6bedfc962c Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:24:50 -0600 Subject: [PATCH 05/10] patch more tests --- packages/pds/src/api/com/atproto/server/createAccount.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index b78b358fb4c..8b58f3dc6b9 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -110,6 +110,7 @@ export default function (server: Server, ctx: AppContext) { // insert invite code use if (ctx.cfg.invites.required && inviteCode) { + await ensureCodeIsAvailable(dbTxn, inviteCode, true) await dbTxn.db .insertInto('invite_code_use') .values({ From 9a1fe0ae340e38a9bf34a4fa942679492356d4f1 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:37:02 -0600 Subject: [PATCH 06/10] fix query --- packages/pds/src/api/com/atproto/server/createAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 8b58f3dc6b9..929654e33b0 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -479,7 +479,7 @@ const ensureUnusedHandleAndEmail = async ( .innerJoin('did_handle', 'did_handle.did', 'user_account.did') .select(['did_handle.handle', 'user_account.email']) .where('user_account.email', '=', email) - .where('did_handle.handle', '=', handle) + .orWhere('did_handle.handle', '=', handle) .executeTakeFirst() if (!res) return if (res.email === email) { From 5d5d7bce7db1e53c447b1b3acfca08519ca1abef Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 11:15:43 -0600 Subject: [PATCH 07/10] tidy account existence check --- .../api/com/atproto/server/createAccount.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 929654e33b0..1fda28e0266 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -11,7 +11,10 @@ import * as scrypt from '../../../../db/scrypt' import { Server } from '../../../../lexicon' import { InputSchema as CreateAccountInput } from '../../../../lexicon/types/com/atproto/server/createAccount' import { countAll } from '../../../../db/util' -import { UserAlreadyExistsError } from '../../../../services/account' +import { + AccountService, + UserAlreadyExistsError, +} from '../../../../services/account' import AppContext from '../../../../context' import Database from '../../../../db' import { didDocForSession } from './util' @@ -37,8 +40,6 @@ export default function (server: Server, ctx: AppContext) { ? await validateInputsForPdsViaEntryway(ctx, input.body) : await validateInputsForPdsViaUser(ctx, input.body) - await ensureUnusedHandleAndEmail(ctx.db, handle, email) - const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) @@ -266,7 +267,7 @@ const validateInputsForPdsViaUser = async ( did: input.did, }) - await ensureUnusedHandleAndEmail(ctx.db, handle, email) + await ensureUnusedHandleAndEmail(ctx.services.account(ctx.db), handle, email) // check that the invite code still has uses if (ctx.cfg.invites.required && inviteCode) { @@ -470,21 +471,17 @@ const reserveSigningKey = async (ctx: AppContext, host: string) => { } const ensureUnusedHandleAndEmail = async ( - db: Database, + accountSrvc: AccountService, handle: string, email: string, ) => { - const res = await db.db - .selectFrom('user_account') - .innerJoin('did_handle', 'did_handle.did', 'user_account.did') - .select(['did_handle.handle', 'user_account.email']) - .where('user_account.email', '=', email) - .orWhere('did_handle.handle', '=', handle) - .executeTakeFirst() - if (!res) return - if (res.email === email) { + const [byHandle, byEmail] = await Promise.all([ + accountSrvc.getAccount(handle, true), + accountSrvc.getAccountByEmail(email, true), + ]) + if (byEmail) { throw new InvalidRequestError(`Email already taken: ${email}`) - } else { + } else if (byHandle) { throw new InvalidRequestError(`Handle already taken: ${handle}`) } } From e8b47b5356483bdb6e4c013dd9db77ec77c390db Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 13:18:41 -0600 Subject: [PATCH 08/10] improve error handling --- packages/pds/src/twilio.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 113c9e18245..3fb5f77ce7e 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -45,7 +45,18 @@ export class TwilioClient { }) } catch (err) { log.error({ err, phoneNumber }, 'error sending twilio code') - throw new UpstreamFailureError('Could not send verification text') + const code = typeof err === 'object' ? err?.['code'] : undefined + if (code === 60200) { + throw new InvalidRequestError( + 'Could not send verification text: invalid phone number', + ) + } else if (code === 60220) { + throw new InvalidRequestError( + `We're sorry, we're not currently able to send verification messages to China. We're working with our providers to solve this as quickly as possible.`, + ) + } else { + throw new UpstreamFailureError('Could not send verification text') + } } } From 021fa9adb5f30a33b37a6effb220ef7fefdb665c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 13:47:35 -0600 Subject: [PATCH 09/10] handle one more error --- packages/pds/src/twilio.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 3fb5f77ce7e..eec698865a3 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -50,9 +50,9 @@ export class TwilioClient { throw new InvalidRequestError( 'Could not send verification text: invalid phone number', ) - } else if (code === 60220) { + } else if (code === 60605 || code === 60220) { throw new InvalidRequestError( - `We're sorry, we're not currently able to send verification messages to China. We're working with our providers to solve this as quickly as possible.`, + `We're sorry, we're not currently able to send verification messages to your country. We're working with our providers to solve this as quickly as possible.`, ) } else { throw new UpstreamFailureError('Could not send verification text') From 49ced7cf20973390a2d2c91396b429102fb7037d Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 29 Jan 2024 16:35:14 -0600 Subject: [PATCH 10/10] dont build branch --- .github/workflows/build-and-push-pds-aws.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 7589f56769a..097f782d88e 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - entryway-twilio env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}