Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* Codegen

* Explicitly add Zod (already a peer dep) and validation to api

* Add Nux methods

* Match naming convention

* Remove id, it won't be used

* Add tests

* Use id instead of name, little clearer

* Update API contracts

* Update tests

* Changeset

* Don't mutate
  • Loading branch information
estrattonbailey authored Sep 11, 2024
1 parent e6bd5ae commit 33aa0c7
Show file tree
Hide file tree
Showing 17 changed files with 478 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/selfish-ads-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@atproto/bsky": patch
"@atproto/api": patch
"@atproto/pds": patch
---

Add NUX API
35 changes: 35 additions & 0 deletions lexicons/app/bsky/actor/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,15 @@
"type": "array",
"maxLength": 1000,
"items": { "type": "string", "maxLength": 100 }
},
"nuxs": {
"description": "Storage for NUXs the user has encountered.",
"type": "array",
"maxLength": 100,
"items": {
"type": "ref",
"ref": "app.bsky.actor.defs#nux"
}
}
}
},
Expand All @@ -429,6 +438,32 @@
"properties": {
"guide": { "type": "string", "maxLength": 100 }
}
},
"nux": {
"type": "object",
"description": "A new user experiences (NUX) storage object",
"required": ["id", "completed"],
"properties": {
"id": {
"type": "string",
"maxLength": 100
},
"completed": {
"type": "boolean",
"default": false
},
"data": {
"description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.",
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300
},
"expiresAt": {
"type": "string",
"format": "datetime",
"description": "The date and time at which the NUX will expire and should be considered completed."
}
}
}
}
}
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"@atproto/xrpc": "workspace:^",
"await-lock": "^2.2.2",
"multiformats": "^9.9.0",
"tlds": "^1.234.0"
"tlds": "^1.234.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@atproto/lex-cli": "workspace:^",
Expand Down
81 changes: 80 additions & 1 deletion packages/api/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ToolsNS,
} from './client/index'
import { schemas } from './client/lexicons'
import { MutedWord } from './client/types/app/bsky/actor/defs'
import { MutedWord, Nux } from './client/types/app/bsky/actor/defs'
import { BSKY_LABELER_DID } from './const'
import { interpretLabelValueDefinitions } from './moderation'
import { DEFAULT_LABEL_SETTINGS } from './moderation/const/labels'
Expand All @@ -40,6 +40,7 @@ import {
sanitizeMutedWordValue,
savedFeedsToUriArrays,
validateSavedFeed,
validateNux,
} from './util'

const FEED_VIEW_PREF_DEFAULTS = {
Expand Down Expand Up @@ -570,6 +571,7 @@ export class Agent extends XrpcClient {
bskyAppState: {
queuedNudges: [],
activeProgressGuide: undefined,
nuxs: [],
},
}
const res = await this.app.bsky.actor.getPreferences({})
Expand Down Expand Up @@ -674,6 +676,7 @@ export class Agent extends XrpcClient {
const { $type, ...v } = pref
prefs.bskyAppState.queuedNudges = v.queuedNudges || []
prefs.bskyAppState.activeProgressGuide = v.activeProgressGuide
prefs.bskyAppState.nuxs = v.nuxs || []
}
}

Expand Down Expand Up @@ -1374,6 +1377,82 @@ export class Agent extends XrpcClient {
})
}

/**
* Insert or update a NUX in user prefs
*/
async bskyAppUpsertNux(nux: Nux) {
validateNux(nux)

await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => {
let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast(
(pref) =>
AppBskyActorDefs.isBskyAppStatePref(pref) &&
AppBskyActorDefs.validateBskyAppStatePref(pref).success,
)

bskyAppStatePref = bskyAppStatePref || {}
bskyAppStatePref.nuxs = bskyAppStatePref.nuxs || []

const existing = bskyAppStatePref.nuxs?.find((n) => {
return n.id === nux.id
})

let next: AppBskyActorDefs.Nux

if (existing) {
next = {
id: existing.id,
completed: nux.completed,
data: nux.data,
expiresAt: nux.expiresAt,
}
} else {
next = nux
}

// remove duplicates and append
bskyAppStatePref.nuxs = bskyAppStatePref.nuxs
.filter((n) => n.id !== nux.id)
.concat(next)

return prefs
.filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
.concat([
{
...bskyAppStatePref,
$type: 'app.bsky.actor.defs#bskyAppStatePref',
},
])
})
}

/**
* Removes NUXs from user preferences.
*/
async bskyAppRemoveNuxs(ids: string[]) {
await this.updatePreferences((prefs: AppBskyActorDefs.Preferences) => {
let bskyAppStatePref: AppBskyActorDefs.BskyAppStatePref = prefs.findLast(
(pref) =>
AppBskyActorDefs.isBskyAppStatePref(pref) &&
AppBskyActorDefs.validateBskyAppStatePref(pref).success,
)

bskyAppStatePref = bskyAppStatePref || {}
bskyAppStatePref.nuxs = (bskyAppStatePref.nuxs || []).filter((nux) => {
return !ids.includes(nux.id)
})

return prefs
.filter((p) => !AppBskyActorDefs.isBskyAppStatePref(p))
.concat([
{
...bskyAppStatePref,
$type: 'app.bsky.actor.defs#bskyAppStatePref',
},
])
})
}

//- Private methods

#prefsLock = new AwaitLock()
Expand Down
37 changes: 37 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4572,6 +4572,15 @@ export const schemaDict = {
maxLength: 100,
},
},
nuxs: {
description: 'Storage for NUXs the user has encountered.',
type: 'array',
maxLength: 100,
items: {
type: 'ref',
ref: 'lex:app.bsky.actor.defs#nux',
},
},
},
},
bskyAppProgressGuide: {
Expand All @@ -4586,6 +4595,34 @@ export const schemaDict = {
},
},
},
nux: {
type: 'object',
description: 'A new user experiences (NUX) storage object',
required: ['id', 'completed'],
properties: {
id: {
type: 'string',
maxLength: 100,
},
completed: {
type: 'boolean',
default: false,
},
data: {
description:
'Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.',
type: 'string',
maxLength: 3000,
maxGraphemes: 300,
},
expiresAt: {
type: 'string',
format: 'datetime',
description:
'The date and time at which the NUX will expire and should be considered completed.',
},
},
},
},
},
AppBskyActorGetPreferences: {
Expand Down
23 changes: 23 additions & 0 deletions packages/api/src/client/types/app/bsky/actor/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ export interface BskyAppStatePref {
activeProgressGuide?: BskyAppProgressGuide
/** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */
queuedNudges?: string[]
/** Storage for NUXs the user has encountered. */
nuxs?: Nux[]
[k: string]: unknown
}

Expand Down Expand Up @@ -501,3 +503,24 @@ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide {
export function validateBskyAppProgressGuide(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v)
}

/** A new user experiences (NUX) storage object */
export interface Nux {
id: string
completed: boolean
/** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */
data?: string
/** The date and time at which the NUX will expire and should be considered completed. */
expiresAt?: string
[k: string]: unknown
}

export function isNux(v: unknown): v is Nux {
return (
isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.actor.defs#nux'
)
}

export function validateNux(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#nux', v)
}
1 change: 1 addition & 0 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,6 @@ export interface BskyPreferences {
bskyAppState: {
queuedNudges: string[]
activeProgressGuide: AppBskyActorDefs.BskyAppProgressGuide | undefined
nuxs: AppBskyActorDefs.Nux[]
}
}
15 changes: 15 additions & 0 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AtUri } from '@atproto/syntax'
import { TID } from '@atproto/common-web'
import zod from 'zod'

import { Nux } from './client/types/app/bsky/actor/defs'
import { AppBskyActorDefs } from './client'

export function sanitizeMutedWordValue(value: string) {
Expand Down Expand Up @@ -94,3 +96,16 @@ export const asDid = (value: string): Did => {
if (isDid(value)) return value
throw new TypeError(`Invalid DID: ${value}`)
}

export const nuxSchema = zod
.object({
id: zod.string().max(64),
completed: zod.boolean(),
data: zod.string().max(300).optional(),
expiresAt: zod.string().datetime().optional(),
})
.strict()

export function validateNux(nux: Nux) {
nuxSchema.parse(nux)
}
Loading

0 comments on commit 33aa0c7

Please sign in to comment.