Notification preferences V2 endpoints (#3901)

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: rafael <rafael@blueskyweb.xyz>
This commit is contained in:
Samuel Newman 2025-06-07 00:29:05 +03:00 committed by GitHub
parent cd4bed3c9e
commit a48671e730
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 3704 additions and 34 deletions

View File

@ -0,0 +1,8 @@
---
"@atproto/ozone": patch
"@atproto/bsky": patch
"@atproto/api": patch
"@atproto/pds": patch
---
Add notification preferences V2 lexicons

View File

@ -5,6 +5,64 @@
"recordDeleted": {
"type": "object",
"properties": {}
},
"chatPreference": {
"type": "object",
"required": ["filter", "push"],
"properties": {
"filter": { "type": "string", "knownValues": ["all", "accepted"] },
"push": { "type": "boolean" }
}
},
"filterablePreference": {
"type": "object",
"required": ["filter", "list", "push"],
"properties": {
"filter": { "type": "string", "knownValues": ["all", "follows"] },
"list": { "type": "boolean" },
"push": { "type": "boolean" }
}
},
"preference": {
"type": "object",
"required": ["list", "push"],
"properties": {
"list": { "type": "boolean" },
"push": { "type": "boolean" }
}
},
"preferences": {
"type": "object",
"required": [
"chat",
"follow",
"like",
"likeViaRepost",
"mention",
"quote",
"reply",
"repost",
"repostViaRepost",
"starterpackJoined",
"subscribedPost",
"unverified",
"verified"
],
"properties": {
"chat": { "type": "ref", "ref": "#chatPreference" },
"follow": { "type": "ref", "ref": "#filterablePreference" },
"like": { "type": "ref", "ref": "#filterablePreference" },
"likeViaRepost": { "type": "ref", "ref": "#filterablePreference" },
"mention": { "type": "ref", "ref": "#filterablePreference" },
"quote": { "type": "ref", "ref": "#filterablePreference" },
"reply": { "type": "ref", "ref": "#filterablePreference" },
"repost": { "type": "ref", "ref": "#filterablePreference" },
"repostViaRepost": { "type": "ref", "ref": "#filterablePreference" },
"starterpackJoined": { "type": "ref", "ref": "#preference" },
"subscribedPost": { "type": "ref", "ref": "#preference" },
"unverified": { "type": "ref", "ref": "#preference" },
"verified": { "type": "ref", "ref": "#preference" }
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"lexicon": 1,
"id": "app.bsky.notification.getPreferences",
"defs": {
"main": {
"type": "query",
"description": "Get notification-related preferences for an account. Requires auth.",
"parameters": {
"type": "params",
"properties": {}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["preferences"],
"properties": {
"preferences": {
"type": "ref",
"ref": "app.bsky.notification.defs#preferences"
}
}
}
}
}
}
}

View File

@ -0,0 +1,83 @@
{
"lexicon": 1,
"id": "app.bsky.notification.putPreferencesV2",
"defs": {
"main": {
"type": "procedure",
"description": "Set notification-related preferences for an account. Requires auth.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"properties": {
"chat": {
"type": "ref",
"ref": "app.bsky.notification.defs#chatPreference"
},
"follow": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"like": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"likeViaRepost": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"mention": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"quote": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"reply": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"repost": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"repostViaRepost": {
"type": "ref",
"ref": "app.bsky.notification.defs#filterablePreference"
},
"starterpackJoined": {
"type": "ref",
"ref": "app.bsky.notification.defs#preference"
},
"subscribedPost": {
"type": "ref",
"ref": "app.bsky.notification.defs#preference"
},
"unverified": {
"type": "ref",
"ref": "app.bsky.notification.defs#preference"
},
"verified": {
"type": "ref",
"ref": "app.bsky.notification.defs#preference"
}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["preferences"],
"properties": {
"preferences": {
"type": "ref",
"ref": "app.bsky.notification.defs#preferences"
}
}
}
}
}
}
}

View File

@ -174,9 +174,11 @@ import * as AppBskyLabelerDefs from './types/app/bsky/labeler/defs.js'
import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'
import * as AppBskyLabelerService from './types/app/bsky/labeler/service.js'
import * as AppBskyNotificationDefs from './types/app/bsky/notification/defs.js'
import * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'
import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'
import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'
import * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'
import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet.js'
@ -437,9 +439,11 @@ export * as AppBskyLabelerDefs from './types/app/bsky/labeler/defs.js'
export * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'
export * as AppBskyLabelerService from './types/app/bsky/labeler/service.js'
export * as AppBskyNotificationDefs from './types/app/bsky/notification/defs.js'
export * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'
export * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'
export * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'
export * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'
export * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
export * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet.js'
@ -3343,6 +3347,18 @@ export class AppBskyNotificationNS {
this._client = client
}
getPreferences(
params?: AppBskyNotificationGetPreferences.QueryParams,
opts?: AppBskyNotificationGetPreferences.CallOptions,
): Promise<AppBskyNotificationGetPreferences.Response> {
return this._client.call(
'app.bsky.notification.getPreferences',
params,
undefined,
opts,
)
}
getUnreadCount(
params?: AppBskyNotificationGetUnreadCount.QueryParams,
opts?: AppBskyNotificationGetUnreadCount.CallOptions,
@ -3379,6 +3395,18 @@ export class AppBskyNotificationNS {
)
}
putPreferencesV2(
data?: AppBskyNotificationPutPreferencesV2.InputSchema,
opts?: AppBskyNotificationPutPreferencesV2.CallOptions,
): Promise<AppBskyNotificationPutPreferencesV2.Response> {
return this._client.call(
'app.bsky.notification.putPreferencesV2',
opts?.qp,
data,
opts,
)
}
registerPush(
data?: AppBskyNotificationRegisterPush.InputSchema,
opts?: AppBskyNotificationRegisterPush.CallOptions,

View File

@ -9732,6 +9732,147 @@ export const schemaDict = {
type: 'object',
properties: {},
},
chatPreference: {
type: 'object',
required: ['filter', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'accepted'],
},
push: {
type: 'boolean',
},
},
},
filterablePreference: {
type: 'object',
required: ['filter', 'list', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'follows'],
},
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preference: {
type: 'object',
required: ['list', 'push'],
properties: {
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preferences: {
type: 'object',
required: [
'chat',
'follow',
'like',
'likeViaRepost',
'mention',
'quote',
'reply',
'repost',
'repostViaRepost',
'starterpackJoined',
'subscribedPost',
'unverified',
'verified',
],
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
},
AppBskyNotificationGetPreferences: {
lexicon: 1,
id: 'app.bsky.notification.getPreferences',
defs: {
main: {
type: 'query',
description:
'Get notification-related preferences for an account. Requires auth.',
parameters: {
type: 'params',
properties: {},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationGetUnreadCount: {
@ -9924,6 +10065,90 @@ export const schemaDict = {
},
},
},
AppBskyNotificationPutPreferencesV2: {
lexicon: 1,
id: 'app.bsky.notification.putPreferencesV2',
defs: {
main: {
type: 'procedure',
description:
'Set notification-related preferences for an account. Requires auth.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationRegisterPush: {
lexicon: 1,
id: 'app.bsky.notification.registerPush',
@ -16557,10 +16782,12 @@ export const ids = {
AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
AppBskyLabelerService: 'app.bsky.labeler.service',
AppBskyNotificationDefs: 'app.bsky.notification.defs',
AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',
AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',
AppBskyNotificationListNotifications:
'app.bsky.notification.listNotifications',
AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',
AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',
AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',
AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',
AppBskyRichtextFacet: 'app.bsky.richtext.facet',

View File

@ -27,3 +27,79 @@ export function isRecordDeleted<V>(v: V) {
export function validateRecordDeleted<V>(v: V) {
return validate<RecordDeleted & V>(v, id, hashRecordDeleted)
}
export interface ChatPreference {
$type?: 'app.bsky.notification.defs#chatPreference'
filter: 'all' | 'accepted' | (string & {})
push: boolean
}
const hashChatPreference = 'chatPreference'
export function isChatPreference<V>(v: V) {
return is$typed(v, id, hashChatPreference)
}
export function validateChatPreference<V>(v: V) {
return validate<ChatPreference & V>(v, id, hashChatPreference)
}
export interface FilterablePreference {
$type?: 'app.bsky.notification.defs#filterablePreference'
filter: 'all' | 'follows' | (string & {})
list: boolean
push: boolean
}
const hashFilterablePreference = 'filterablePreference'
export function isFilterablePreference<V>(v: V) {
return is$typed(v, id, hashFilterablePreference)
}
export function validateFilterablePreference<V>(v: V) {
return validate<FilterablePreference & V>(v, id, hashFilterablePreference)
}
export interface Preference {
$type?: 'app.bsky.notification.defs#preference'
list: boolean
push: boolean
}
const hashPreference = 'preference'
export function isPreference<V>(v: V) {
return is$typed(v, id, hashPreference)
}
export function validatePreference<V>(v: V) {
return validate<Preference & V>(v, id, hashPreference)
}
export interface Preferences {
$type?: 'app.bsky.notification.defs#preferences'
chat: ChatPreference
follow: FilterablePreference
like: FilterablePreference
likeViaRepost: FilterablePreference
mention: FilterablePreference
quote: FilterablePreference
reply: FilterablePreference
repost: FilterablePreference
repostViaRepost: FilterablePreference
starterpackJoined: Preference
subscribedPost: Preference
unverified: Preference
verified: Preference
}
const hashPreferences = 'preferences'
export function isPreferences<V>(v: V) {
return is$typed(v, id, hashPreferences)
}
export function validatePreferences<V>(v: V) {
return validate<Preferences & V>(v, id, hashPreferences)
}

View File

@ -0,0 +1,40 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { HeadersMap, XRPCError } from '@atproto/xrpc'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.getPreferences'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
export interface CallOptions {
signal?: AbortSignal
headers?: HeadersMap
}
export interface Response {
success: boolean
headers: HeadersMap
data: OutputSchema
}
export function toKnownErr(e: any) {
return e
}

View File

@ -0,0 +1,56 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { HeadersMap, XRPCError } from '@atproto/xrpc'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.putPreferencesV2'
export interface QueryParams {}
export interface InputSchema {
chat?: AppBskyNotificationDefs.ChatPreference
follow?: AppBskyNotificationDefs.FilterablePreference
like?: AppBskyNotificationDefs.FilterablePreference
likeViaRepost?: AppBskyNotificationDefs.FilterablePreference
mention?: AppBskyNotificationDefs.FilterablePreference
quote?: AppBskyNotificationDefs.FilterablePreference
reply?: AppBskyNotificationDefs.FilterablePreference
repost?: AppBskyNotificationDefs.FilterablePreference
repostViaRepost?: AppBskyNotificationDefs.FilterablePreference
starterpackJoined?: AppBskyNotificationDefs.Preference
subscribedPost?: AppBskyNotificationDefs.Preference
unverified?: AppBskyNotificationDefs.Preference
verified?: AppBskyNotificationDefs.Preference
}
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
export interface CallOptions {
signal?: AbortSignal
headers?: HeadersMap
qp?: QueryParams
encoding?: 'application/json'
}
export interface Response {
success: boolean
headers: HeadersMap
data: OutputSchema
}
export function toKnownErr(e: any) {
return e
}

View File

@ -753,6 +753,66 @@ message GetBlocklistSubscriptionsResponse {
// Notifications
//
message GetNotificationPreferencesRequest {
repeated string dids = 1;
}
message NotificationChannelList {
bool enabled = 1;
}
message NotificationChannelPush {
bool enabled = 1;
}
enum NotificationFilter {
NOTIFICATION_FILTER_UNSPECIFIED = 0;
NOTIFICATION_FILTER_ALL = 1;
NOTIFICATION_FILTER_FOLLOWS = 2;
}
message FilterableNotificationPreference {
NotificationFilter filter = 1;
NotificationChannelList list = 2;
NotificationChannelPush push = 3;
}
message NotificationPreference {
NotificationChannelList list = 1;
NotificationChannelPush push = 2;
}
enum ChatNotificationFilter {
CHAT_NOTIFICATION_FILTER_UNSPECIFIED = 0;
CHAT_NOTIFICATION_FILTER_ALL = 1;
CHAT_NOTIFICATION_FILTER_ACCEPTED = 2;
}
message ChatNotificationPreference {
ChatNotificationFilter filter = 1;
NotificationChannelPush push = 2;
}
message NotificationPreferences {
ChatNotificationPreference chat = 1;
FilterableNotificationPreference follow = 2;
FilterableNotificationPreference like = 3;
FilterableNotificationPreference like_via_repost = 4;
FilterableNotificationPreference mention = 5;
FilterableNotificationPreference quote = 6;
FilterableNotificationPreference reply = 7;
FilterableNotificationPreference repost = 8;
FilterableNotificationPreference repost_via_repost = 9;
NotificationPreference starterpack_joined = 10;
NotificationPreference subscribed_post = 11;
NotificationPreference unverified = 12;
NotificationPreference verified = 13;
}
message GetNotificationPreferencesResponse {
repeated NotificationPreferences preferences = 1;
}
// - list recent notifications for a user
// - notifications should include a uri for the record that caused the notif & a reason for the notification (reply, like, quotepost, etc)
// - this should include both read & unread notifs
@ -1256,6 +1316,7 @@ service Service {
rpc GetBlocklistSubscriptions(GetBlocklistSubscriptionsRequest) returns (GetBlocklistSubscriptionsResponse);
// Notifications
rpc GetNotificationPreferences(GetNotificationPreferencesRequest) returns (GetNotificationPreferencesResponse);
rpc GetNotifications(GetNotificationsRequest) returns (GetNotificationsResponse);
rpc GetNotificationSeen(GetNotificationSeenRequest) returns (GetNotificationSeenResponse);
rpc GetUnreadNotificationCount(GetUnreadNotificationCountRequest) returns (GetUnreadNotificationCountResponse);

View File

@ -0,0 +1,50 @@
import assert from 'node:assert'
import { Un$Typed } from '@atproto/api'
import { UpstreamFailureError } from '@atproto/xrpc-server'
import { AppContext } from '../../../../context'
import { Server } from '../../../../lexicon'
import { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'
import { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'
import { protobufToLex } from './util'
export default function (server: Server, ctx: AppContext) {
server.app.bsky.notification.getPreferences({
auth: ctx.authVerifier.standard,
handler: async ({ auth }) => {
const actorDid = auth.credentials.iss
const preferences = await computePreferences(ctx, actorDid)
return {
encoding: 'application/json',
body: {
preferences,
},
}
},
})
}
const computePreferences = async (
ctx: AppContext,
actorDid: string,
): Promise<Un$Typed<Preferences>> => {
let res: GetNotificationPreferencesResponse
try {
res = await ctx.dataplane.getNotificationPreferences({
dids: [actorDid],
})
} catch (err) {
throw new UpstreamFailureError(
'cannot get current notification preferences',
'NotificationPreferencesFailed',
{ cause: err },
)
}
assert(
res.preferences.length === 1,
`expected exactly one preferences entry, got ${res.preferences.length}`,
)
const currentPreferences = protobufToLex(res.preferences[0])
return currentPreferences
}

View File

@ -0,0 +1,62 @@
import assert from 'node:assert'
import { Un$Typed } from '@atproto/api'
import { UpstreamFailureError } from '@atproto/xrpc-server'
import { AppContext } from '../../../../context'
import { Server } from '../../../../lexicon'
import { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'
import { HandlerInput } from '../../../../lexicon/types/app/bsky/notification/putPreferencesV2'
import { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'
import { protobufToLex } from './util'
export default function (server: Server, ctx: AppContext) {
server.app.bsky.notification.putPreferencesV2({
auth: ctx.authVerifier.standard,
handler: async ({ auth, input }) => {
const actorDid = auth.credentials.iss
const preferences = await computePreferences(ctx, actorDid, input)
// Notification preferences are created automatically on the dataplane on signup, so we just update.
await ctx.stashClient.update({
actorDid,
namespace: 'app.bsky.notification.defs#preferences',
key: 'self',
payload: preferences,
})
return {
encoding: 'application/json',
body: {
preferences,
},
}
},
})
}
const computePreferences = async (
ctx: AppContext,
actorDid: string,
input: HandlerInput,
): Promise<Un$Typed<Preferences>> => {
let res: GetNotificationPreferencesResponse
try {
res = await ctx.dataplane.getNotificationPreferences({
dids: [actorDid],
})
} catch (err) {
throw new UpstreamFailureError(
'cannot get current notification preferences',
'NotificationPreferencesFailed',
{ cause: err },
)
}
assert(
res.preferences.length === 1,
`expected exactly one preferences entry, got ${res.preferences.length}`,
)
const currentPreferences = protobufToLex(res.preferences[0])
const preferences = { ...currentPreferences, ...input.body }
return preferences
}

View File

@ -0,0 +1,123 @@
import { Un$Typed } from '@atproto/api'
import {
ChatPreference,
FilterablePreference,
Preference,
Preferences,
} from '../../../../lexicon/types/app/bsky/notification/defs'
import {
ChatNotificationFilter,
ChatNotificationPreference,
FilterableNotificationPreference,
NotificationFilter,
NotificationPreference,
NotificationPreferences,
} from '../../../../proto/bsky_pb'
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>
}
: T
const ensureChatPreference = (
p?: DeepPartial<ChatPreference>,
): ChatPreference => {
const filters = ['all', 'accepted']
return {
filter:
typeof p?.filter === 'string' && filters.includes(p.filter)
? p.filter
: 'all',
push: p?.push ?? true,
}
}
const ensureFilterablePreference = (
p?: DeepPartial<FilterablePreference>,
): FilterablePreference => {
const filters = ['all', 'follows']
return {
filter:
typeof p?.filter === 'string' && filters.includes(p.filter)
? p.filter
: 'all',
list: p?.list ?? true,
push: p?.push ?? true,
}
}
const ensurePreference = (p?: DeepPartial<Preference>): Preference => {
return {
list: p?.list ?? true,
push: p?.push ?? true,
}
}
const ensurePreferences = (
p: DeepPartial<Preferences>,
): Un$Typed<Preferences> => {
return {
chat: ensureChatPreference(p.chat),
follow: ensureFilterablePreference(p.follow),
like: ensureFilterablePreference(p.like),
likeViaRepost: ensureFilterablePreference(p.likeViaRepost),
mention: ensureFilterablePreference(p.mention),
quote: ensureFilterablePreference(p.quote),
reply: ensureFilterablePreference(p.reply),
repost: ensureFilterablePreference(p.repost),
repostViaRepost: ensureFilterablePreference(p.repostViaRepost),
starterpackJoined: ensurePreference(p.starterpackJoined),
subscribedPost: ensurePreference(p.subscribedPost),
unverified: ensurePreference(p.unverified),
verified: ensurePreference(p.verified),
}
}
const protobufChatPreferenceToLex = (
p?: DeepPartial<ChatNotificationPreference>,
): DeepPartial<ChatPreference> => {
return {
filter: p?.filter === ChatNotificationFilter.ACCEPTED ? 'accepted' : 'all',
push: p?.push?.enabled,
}
}
const protobufFilterablePreferenceToLex = (
p?: DeepPartial<FilterableNotificationPreference>,
): DeepPartial<FilterablePreference> => {
return {
filter: p?.filter === NotificationFilter.FOLLOWS ? 'follows' : 'all',
list: p?.list?.enabled,
push: p?.push?.enabled,
}
}
const protobufPreferenceToLex = (
p?: DeepPartial<NotificationPreference>,
): DeepPartial<Preference> => {
return {
list: p?.list?.enabled,
push: p?.push?.enabled,
}
}
export const protobufToLex = (
res: DeepPartial<NotificationPreferences>,
): Un$Typed<Preferences> => {
return ensurePreferences({
chat: protobufChatPreferenceToLex(res.chat),
follow: protobufFilterablePreferenceToLex(res.follow),
like: protobufFilterablePreferenceToLex(res.like),
likeViaRepost: protobufFilterablePreferenceToLex(res.likeViaRepost),
mention: protobufFilterablePreferenceToLex(res.mention),
quote: protobufFilterablePreferenceToLex(res.quote),
reply: protobufFilterablePreferenceToLex(res.reply),
repost: protobufFilterablePreferenceToLex(res.repost),
repostViaRepost: protobufFilterablePreferenceToLex(res.repostViaRepost),
starterpackJoined: protobufPreferenceToLex(res.starterpackJoined),
subscribedPost: protobufPreferenceToLex(res.subscribedPost),
unverified: protobufPreferenceToLex(res.unverified),
verified: protobufPreferenceToLex(res.verified),
})
}

View File

@ -42,9 +42,11 @@ import unmuteActor from './app/bsky/graph/unmuteActor'
import unmuteActorList from './app/bsky/graph/unmuteActorList'
import unmuteThread from './app/bsky/graph/unmuteThread'
import getLabelerServices from './app/bsky/labeler/getServices'
import getPreferences from './app/bsky/notification/getPreferences'
import getUnreadCount from './app/bsky/notification/getUnreadCount'
import listNotifications from './app/bsky/notification/listNotifications'
import putPreferences from './app/bsky/notification/putPreferences'
import putPreferencesV2 from './app/bsky/notification/putPreferencesV2'
import registerPush from './app/bsky/notification/registerPush'
import updateSeen from './app/bsky/notification/updateSeen'
import getConfig from './app/bsky/unspecced/getConfig'
@ -122,10 +124,12 @@ export default function (server: Server, ctx: AppContext) {
searchActors(server, ctx)
searchActorsTypeahead(server, ctx)
getSuggestions(server, ctx)
getPreferences(server, ctx)
getUnreadCount(server, ctx)
listNotifications(server, ctx)
updateSeen(server, ctx)
putPreferences(server, ctx)
putPreferencesV2(server, ctx)
registerPush(server, ctx)
getConfig(server, ctx)
getPopularFeedGenerators(server, ctx)

View File

@ -13,6 +13,7 @@ import { DataPlaneClient, HostList } from './data-plane/client'
import { FeatureGates } from './feature-gates'
import { Hydrator } from './hydration/hydrator'
import { httpLogger as log } from './logger'
import { StashClient } from './stash'
import {
ParsedLabelers,
defaultLabelerHeader,
@ -35,6 +36,7 @@ export class AppContext {
signingKey: Keypair
idResolver: IdResolver
bsyncClient: BsyncClient
stashClient: StashClient
courierClient: CourierClient | undefined
authVerifier: AuthVerifier
featureGates: FeatureGates
@ -94,6 +96,10 @@ export class AppContext {
return this.opts.bsyncClient
}
get stashClient(): StashClient {
return this.opts.stashClient
}
get courierClient(): CourierClient | undefined {
return this.opts.courierClient
}

View File

@ -4,10 +4,15 @@ import http from 'node:http'
import { ConnectRouter } from '@connectrpc/connect'
import { expressConnectMiddleware } from '@connectrpc/connect-express'
import express from 'express'
import { TID } from '@atproto/common'
import { AtUri } from '@atproto/syntax'
import { ids } from '../../lexicon/lexicons'
import { Service } from '../../proto/bsync_connect'
import { MuteOperation_Type } from '../../proto/bsync_pb'
import {
Method,
MuteOperation_Type,
PutOperationRequest,
} from '../../proto/bsync_pb'
import { Database } from '../server/db'
export class MockBsync {
@ -138,7 +143,110 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
throw new Error('not implemented')
},
async putOperation(req) {
const { actorDid, namespace, key, method, payload } = req
if (
method !== Method.CREATE &&
method !== Method.UPDATE &&
method !== Method.DELETE
) {
throw new Error(`Unsupported method: ${method}`)
}
const now = new Date().toISOString()
if (namespace === 'app.bsky.notification.defs#preferences') {
await handleNotificationPreferencesOperation(db, req, now)
} else {
await handleGenericOperation(db, req, now)
}
return {
operation: {
id: TID.nextStr(),
actorDid,
namespace,
key,
method,
payload,
},
}
},
async scanOperations() {
throw new Error('not implemented')
},
async ping() {
return {}
},
})
const handleNotificationPreferencesOperation = async (
db: Database,
req: PutOperationRequest,
now: string,
) => {
const { actorDid, namespace, key, method, payload } = req
if (method === Method.CREATE || method === Method.UPDATE) {
return db.db
.insertInto('private_data')
.values({
actorDid,
namespace,
key,
payload: Buffer.from(payload).toString('utf8'),
indexedAt: now,
updatedAt: now,
})
.onConflict((oc) =>
oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
payload: Buffer.from(payload).toString('utf8'),
updatedAt: now,
}),
)
.execute()
}
return handleGenericOperation(db, req, now)
}
const handleGenericOperation = async (
db: Database,
req: PutOperationRequest,
now: string,
) => {
const { actorDid, namespace, key, method, payload } = req
if (method === Method.CREATE) {
return db.db
.insertInto('private_data')
.values({
actorDid,
namespace,
key,
payload: Buffer.from(payload).toString('utf8'),
indexedAt: now,
updatedAt: now,
})
.execute()
}
if (method === Method.UPDATE) {
return db.db
.updateTable('private_data')
.where('actorDid', '=', actorDid)
.where('namespace', '=', namespace)
.where('key', '=', key)
.set({
payload: Buffer.from(payload).toString('utf8'),
updatedAt: now,
})
.execute()
}
return db.db
.deleteFrom('private_data')
.where('actorDid', '=', actorDid)
.where('namespace', '=', namespace)
.where('key', '=', key)
.execute()
}

View File

@ -24,6 +24,7 @@ import * as post from './tables/post'
import * as postAgg from './tables/post-agg'
import * as postEmbed from './tables/post-embed'
import * as postgate from './tables/post-gate'
import * as privateData from './tables/private-data'
import * as profile from './tables/profile'
import * as profileAgg from './tables/profile-agg'
import * as quote from './tables/quote'
@ -77,6 +78,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB &
starterPack.PartialDB &
taggedSuggestion.PartialDB &
quote.PartialDB &
verification.PartialDB
verification.PartialDB &
privateData.PartialDB
export type DatabaseSchema = Kysely<DatabaseSchemaType>

View File

@ -0,0 +1,22 @@
import { Kysely } from 'kysely'
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('private_data')
.addColumn('actorDid', 'varchar', (col) => col.notNull())
.addColumn('namespace', 'varchar', (col) => col.notNull())
.addColumn('key', 'varchar', (col) => col.notNull())
.addColumn('payload', 'text', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
.addPrimaryKeyConstraint('private_data_pkey', [
'actorDid',
'namespace',
'key',
])
.execute()
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('private_data').execute()
}

View File

@ -50,3 +50,4 @@ export * as _20250207T174822012Z from './20250207T174822012Z-add-label-exp'
export * as _20250404T163421487Z from './20250404T163421487Z-verifications'
export * as _20250526T023712742Z from './20250526T023712742Z-like-repost-via'
export * as _20250528T221913281Z from './20250528T221913281Z-add-record-tags'
export * as _20250602T190357447Z from './20250602T190357447Z-add-private-data'

View File

@ -0,0 +1,13 @@
export interface PrivateData {
actorDid: string
namespace: string
key: string
// JSON-encoded
payload: string
indexedAt: string
updatedAt: string
}
export const tableName = 'private_data'
export type PartialDB = { [tableName]: PrivateData }

View File

@ -14,6 +14,7 @@ import lists from './lists'
import moderation from './moderation'
import mutes from './mutes'
import notifs from './notifs'
import privateData from './private-data'
import profile from './profile'
import quotes from './quotes'
import records from './records'
@ -40,6 +41,7 @@ export default (db: Database, idResolver: IdResolver) =>
...moderation(db),
...mutes(db),
...notifs(db),
...privateData(db),
...profile(db),
...quotes(db),
...records(db),

View File

@ -0,0 +1,90 @@
import { ServiceImpl } from '@connectrpc/connect'
import { keyBy } from '@atproto/common'
import {
ChatPreference,
FilterablePreference,
Preference,
Preferences,
} from '../../../lexicon/types/app/bsky/notification/defs'
import { Service } from '../../../proto/bsky_connect'
import {
ChatNotificationFilter,
ChatNotificationPreference,
FilterableNotificationPreference,
NotificationFilter,
NotificationPreference,
NotificationPreferences,
} from '../../../proto/bsky_pb'
import { Database } from '../db'
export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
async getNotificationPreferences(req) {
const { dids } = req
const res = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', 'in', dids)
.where('namespace', '=', 'app.bsky.notification.defs#preferences')
.where('key', '=', 'self')
.execute()
const byDid = keyBy(res, 'actorDid')
const preferences = dids.map((did) => {
const row = byDid.get(did)
if (!row) {
return {}
}
const p: Preferences = JSON.parse(row.payload)
return lexToProtobuf(p)
})
return { preferences }
},
})
export const lexToProtobuf = (p: Preferences): NotificationPreferences => {
return new NotificationPreferences({
chat: lexChatPreferenceToProtobuf(p.chat),
follow: lexFilterablePreferenceToProtobuf(p.follow),
like: lexFilterablePreferenceToProtobuf(p.like),
likeViaRepost: lexFilterablePreferenceToProtobuf(p.likeViaRepost),
mention: lexFilterablePreferenceToProtobuf(p.mention),
quote: lexFilterablePreferenceToProtobuf(p.quote),
reply: lexFilterablePreferenceToProtobuf(p.reply),
repost: lexFilterablePreferenceToProtobuf(p.repost),
repostViaRepost: lexFilterablePreferenceToProtobuf(p.repostViaRepost),
starterpackJoined: lexPreferenceToProtobuf(p.starterpackJoined),
subscribedPost: lexPreferenceToProtobuf(p.subscribedPost),
unverified: lexPreferenceToProtobuf(p.unverified),
verified: lexPreferenceToProtobuf(p.verified),
})
}
const lexChatPreferenceToProtobuf = (
p: ChatPreference,
): ChatNotificationPreference =>
new ChatNotificationPreference({
filter:
p.filter === 'accepted'
? ChatNotificationFilter.ACCEPTED
: ChatNotificationFilter.ALL,
push: { enabled: p.push ?? true },
})
const lexFilterablePreferenceToProtobuf = (
p: FilterablePreference,
): FilterableNotificationPreference =>
new FilterableNotificationPreference({
filter:
p.filter === 'follows'
? NotificationFilter.FOLLOWS
: NotificationFilter.ALL,
list: { enabled: p.list ?? true },
push: { enabled: p.push ?? true },
})
const lexPreferenceToProtobuf = (p: Preference): NotificationPreference =>
new NotificationPreference({
list: { enabled: p.list ?? true },
push: { enabled: p.push ?? true },
})

View File

@ -29,6 +29,7 @@ import * as imageServer from './image/server'
import { ImageUriBuilder } from './image/uri'
import { createServer } from './lexicon'
import { loggerMiddleware } from './logger'
import { createStashClient } from './stash'
import { Views } from './views'
import { VideoUriBuilder } from './views/util'
@ -136,6 +137,8 @@ export class BskyAppView {
interceptors: config.bsyncApiKey ? [bsyncAuth(config.bsyncApiKey)] : [],
})
const stashClient = createStashClient(bsyncClient)
const courierClient = config.courierUrl
? createCourierClient({
baseUrl: config.courierUrl,
@ -178,6 +181,7 @@ export class BskyAppView {
signingKey,
idResolver,
bsyncClient,
stashClient,
courierClient,
authVerifier,
featureGates,

View File

@ -138,9 +138,11 @@ import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'
import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'
import * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'
import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'
import * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'
import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'
import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'
import * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'
import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
import * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'
@ -1853,6 +1855,17 @@ export class AppBskyNotificationNS {
this._server = server
}
getPreferences<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationGetPreferences.Handler<ExtractAuth<AV>>,
AppBskyNotificationGetPreferences.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getUnreadCount<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
@ -1886,6 +1899,17 @@ export class AppBskyNotificationNS {
return this._server.xrpc.method(nsid, cfg)
}
putPreferencesV2<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationPutPreferencesV2.Handler<ExtractAuth<AV>>,
AppBskyNotificationPutPreferencesV2.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
registerPush<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,

View File

@ -9732,6 +9732,147 @@ export const schemaDict = {
type: 'object',
properties: {},
},
chatPreference: {
type: 'object',
required: ['filter', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'accepted'],
},
push: {
type: 'boolean',
},
},
},
filterablePreference: {
type: 'object',
required: ['filter', 'list', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'follows'],
},
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preference: {
type: 'object',
required: ['list', 'push'],
properties: {
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preferences: {
type: 'object',
required: [
'chat',
'follow',
'like',
'likeViaRepost',
'mention',
'quote',
'reply',
'repost',
'repostViaRepost',
'starterpackJoined',
'subscribedPost',
'unverified',
'verified',
],
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
},
AppBskyNotificationGetPreferences: {
lexicon: 1,
id: 'app.bsky.notification.getPreferences',
defs: {
main: {
type: 'query',
description:
'Get notification-related preferences for an account. Requires auth.',
parameters: {
type: 'params',
properties: {},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationGetUnreadCount: {
@ -9924,6 +10065,90 @@ export const schemaDict = {
},
},
},
AppBskyNotificationPutPreferencesV2: {
lexicon: 1,
id: 'app.bsky.notification.putPreferencesV2',
defs: {
main: {
type: 'procedure',
description:
'Set notification-related preferences for an account. Requires auth.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationRegisterPush: {
lexicon: 1,
id: 'app.bsky.notification.registerPush',
@ -12939,10 +13164,12 @@ export const ids = {
AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
AppBskyLabelerService: 'app.bsky.labeler.service',
AppBskyNotificationDefs: 'app.bsky.notification.defs',
AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',
AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',
AppBskyNotificationListNotifications:
'app.bsky.notification.listNotifications',
AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',
AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',
AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',
AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',
AppBskyRichtextFacet: 'app.bsky.richtext.facet',

View File

@ -27,3 +27,79 @@ export function isRecordDeleted<V>(v: V) {
export function validateRecordDeleted<V>(v: V) {
return validate<RecordDeleted & V>(v, id, hashRecordDeleted)
}
export interface ChatPreference {
$type?: 'app.bsky.notification.defs#chatPreference'
filter: 'all' | 'accepted' | (string & {})
push: boolean
}
const hashChatPreference = 'chatPreference'
export function isChatPreference<V>(v: V) {
return is$typed(v, id, hashChatPreference)
}
export function validateChatPreference<V>(v: V) {
return validate<ChatPreference & V>(v, id, hashChatPreference)
}
export interface FilterablePreference {
$type?: 'app.bsky.notification.defs#filterablePreference'
filter: 'all' | 'follows' | (string & {})
list: boolean
push: boolean
}
const hashFilterablePreference = 'filterablePreference'
export function isFilterablePreference<V>(v: V) {
return is$typed(v, id, hashFilterablePreference)
}
export function validateFilterablePreference<V>(v: V) {
return validate<FilterablePreference & V>(v, id, hashFilterablePreference)
}
export interface Preference {
$type?: 'app.bsky.notification.defs#preference'
list: boolean
push: boolean
}
const hashPreference = 'preference'
export function isPreference<V>(v: V) {
return is$typed(v, id, hashPreference)
}
export function validatePreference<V>(v: V) {
return validate<Preference & V>(v, id, hashPreference)
}
export interface Preferences {
$type?: 'app.bsky.notification.defs#preferences'
chat: ChatPreference
follow: FilterablePreference
like: FilterablePreference
likeViaRepost: FilterablePreference
mention: FilterablePreference
quote: FilterablePreference
reply: FilterablePreference
repost: FilterablePreference
repostViaRepost: FilterablePreference
starterpackJoined: Preference
subscribedPost: Preference
unverified: Preference
verified: Preference
}
const hashPreferences = 'preferences'
export function isPreferences<V>(v: V) {
return is$typed(v, id, hashPreferences)
}
export function validatePreferences<V>(v: V) {
return validate<Preferences & V>(v, id, hashPreferences)
}

View File

@ -0,0 +1,52 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.getPreferences'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
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 | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput

View File

@ -0,0 +1,69 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.putPreferencesV2'
export interface QueryParams {}
export interface InputSchema {
chat?: AppBskyNotificationDefs.ChatPreference
follow?: AppBskyNotificationDefs.FilterablePreference
like?: AppBskyNotificationDefs.FilterablePreference
likeViaRepost?: AppBskyNotificationDefs.FilterablePreference
mention?: AppBskyNotificationDefs.FilterablePreference
quote?: AppBskyNotificationDefs.FilterablePreference
reply?: AppBskyNotificationDefs.FilterablePreference
repost?: AppBskyNotificationDefs.FilterablePreference
repostViaRepost?: AppBskyNotificationDefs.FilterablePreference
starterpackJoined?: AppBskyNotificationDefs.Preference
subscribedPost?: AppBskyNotificationDefs.Preference
unverified?: AppBskyNotificationDefs.Preference
verified?: AppBskyNotificationDefs.Preference
}
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
headers?: { [key: string]: string }
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput

View File

@ -126,6 +126,8 @@ import {
GetMutesResponse,
GetNewUserCountForRangeRequest,
GetNewUserCountForRangeResponse,
GetNotificationPreferencesRequest,
GetNotificationPreferencesResponse,
GetNotificationSeenRequest,
GetNotificationSeenResponse,
GetNotificationsRequest,
@ -732,6 +734,15 @@ export const Service = {
/**
* Notifications
*
* @generated from rpc bsky.Service.GetNotificationPreferences
*/
getNotificationPreferences: {
name: 'GetNotificationPreferences',
I: GetNotificationPreferencesRequest,
O: GetNotificationPreferencesResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc bsky.Service.GetNotifications
*/
getNotifications: {

View File

@ -13,6 +13,58 @@ import type {
} from '@bufbuild/protobuf'
import { Message, proto3, protoInt64, Timestamp } from '@bufbuild/protobuf'
/**
* @generated from enum bsky.NotificationFilter
*/
export enum NotificationFilter {
/**
* @generated from enum value: NOTIFICATION_FILTER_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: NOTIFICATION_FILTER_ALL = 1;
*/
ALL = 1,
/**
* @generated from enum value: NOTIFICATION_FILTER_FOLLOWS = 2;
*/
FOLLOWS = 2,
}
// Retrieve enum metadata with: proto3.getEnumType(NotificationFilter)
proto3.util.setEnumType(NotificationFilter, 'bsky.NotificationFilter', [
{ no: 0, name: 'NOTIFICATION_FILTER_UNSPECIFIED' },
{ no: 1, name: 'NOTIFICATION_FILTER_ALL' },
{ no: 2, name: 'NOTIFICATION_FILTER_FOLLOWS' },
])
/**
* @generated from enum bsky.ChatNotificationFilter
*/
export enum ChatNotificationFilter {
/**
* @generated from enum value: CHAT_NOTIFICATION_FILTER_UNSPECIFIED = 0;
*/
UNSPECIFIED = 0,
/**
* @generated from enum value: CHAT_NOTIFICATION_FILTER_ALL = 1;
*/
ALL = 1,
/**
* @generated from enum value: CHAT_NOTIFICATION_FILTER_ACCEPTED = 2;
*/
ACCEPTED = 2,
}
// Retrieve enum metadata with: proto3.getEnumType(ChatNotificationFilter)
proto3.util.setEnumType(ChatNotificationFilter, 'bsky.ChatNotificationFilter', [
{ no: 0, name: 'CHAT_NOTIFICATION_FILTER_UNSPECIFIED' },
{ no: 1, name: 'CHAT_NOTIFICATION_FILTER_ALL' },
{ no: 2, name: 'CHAT_NOTIFICATION_FILTER_ACCEPTED' },
])
/**
* @generated from enum bsky.FeedType
*/
@ -8180,6 +8232,623 @@ export class GetBlocklistSubscriptionsResponse extends Message<GetBlocklistSubsc
}
}
/**
* @generated from message bsky.GetNotificationPreferencesRequest
*/
export class GetNotificationPreferencesRequest extends Message<GetNotificationPreferencesRequest> {
/**
* @generated from field: repeated string dids = 1;
*/
dids: string[] = []
constructor(data?: PartialMessage<GetNotificationPreferencesRequest>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.GetNotificationPreferencesRequest'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{
no: 1,
name: 'dids',
kind: 'scalar',
T: 9 /* ScalarType.STRING */,
repeated: true,
},
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): GetNotificationPreferencesRequest {
return new GetNotificationPreferencesRequest().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): GetNotificationPreferencesRequest {
return new GetNotificationPreferencesRequest().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): GetNotificationPreferencesRequest {
return new GetNotificationPreferencesRequest().fromJsonString(
jsonString,
options,
)
}
static equals(
a:
| GetNotificationPreferencesRequest
| PlainMessage<GetNotificationPreferencesRequest>
| undefined,
b:
| GetNotificationPreferencesRequest
| PlainMessage<GetNotificationPreferencesRequest>
| undefined,
): boolean {
return proto3.util.equals(GetNotificationPreferencesRequest, a, b)
}
}
/**
* @generated from message bsky.NotificationChannelList
*/
export class NotificationChannelList extends Message<NotificationChannelList> {
/**
* @generated from field: bool enabled = 1;
*/
enabled = false
constructor(data?: PartialMessage<NotificationChannelList>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.NotificationChannelList'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'enabled', kind: 'scalar', T: 8 /* ScalarType.BOOL */ },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): NotificationChannelList {
return new NotificationChannelList().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): NotificationChannelList {
return new NotificationChannelList().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): NotificationChannelList {
return new NotificationChannelList().fromJsonString(jsonString, options)
}
static equals(
a:
| NotificationChannelList
| PlainMessage<NotificationChannelList>
| undefined,
b:
| NotificationChannelList
| PlainMessage<NotificationChannelList>
| undefined,
): boolean {
return proto3.util.equals(NotificationChannelList, a, b)
}
}
/**
* @generated from message bsky.NotificationChannelPush
*/
export class NotificationChannelPush extends Message<NotificationChannelPush> {
/**
* @generated from field: bool enabled = 1;
*/
enabled = false
constructor(data?: PartialMessage<NotificationChannelPush>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.NotificationChannelPush'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'enabled', kind: 'scalar', T: 8 /* ScalarType.BOOL */ },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): NotificationChannelPush {
return new NotificationChannelPush().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): NotificationChannelPush {
return new NotificationChannelPush().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): NotificationChannelPush {
return new NotificationChannelPush().fromJsonString(jsonString, options)
}
static equals(
a:
| NotificationChannelPush
| PlainMessage<NotificationChannelPush>
| undefined,
b:
| NotificationChannelPush
| PlainMessage<NotificationChannelPush>
| undefined,
): boolean {
return proto3.util.equals(NotificationChannelPush, a, b)
}
}
/**
* @generated from message bsky.FilterableNotificationPreference
*/
export class FilterableNotificationPreference extends Message<FilterableNotificationPreference> {
/**
* @generated from field: bsky.NotificationFilter filter = 1;
*/
filter = NotificationFilter.UNSPECIFIED
/**
* @generated from field: bsky.NotificationChannelList list = 2;
*/
list?: NotificationChannelList
/**
* @generated from field: bsky.NotificationChannelPush push = 3;
*/
push?: NotificationChannelPush
constructor(data?: PartialMessage<FilterableNotificationPreference>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.FilterableNotificationPreference'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{
no: 1,
name: 'filter',
kind: 'enum',
T: proto3.getEnumType(NotificationFilter),
},
{ no: 2, name: 'list', kind: 'message', T: NotificationChannelList },
{ no: 3, name: 'push', kind: 'message', T: NotificationChannelPush },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): FilterableNotificationPreference {
return new FilterableNotificationPreference().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): FilterableNotificationPreference {
return new FilterableNotificationPreference().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): FilterableNotificationPreference {
return new FilterableNotificationPreference().fromJsonString(
jsonString,
options,
)
}
static equals(
a:
| FilterableNotificationPreference
| PlainMessage<FilterableNotificationPreference>
| undefined,
b:
| FilterableNotificationPreference
| PlainMessage<FilterableNotificationPreference>
| undefined,
): boolean {
return proto3.util.equals(FilterableNotificationPreference, a, b)
}
}
/**
* @generated from message bsky.NotificationPreference
*/
export class NotificationPreference extends Message<NotificationPreference> {
/**
* @generated from field: bsky.NotificationChannelList list = 1;
*/
list?: NotificationChannelList
/**
* @generated from field: bsky.NotificationChannelPush push = 2;
*/
push?: NotificationChannelPush
constructor(data?: PartialMessage<NotificationPreference>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.NotificationPreference'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'list', kind: 'message', T: NotificationChannelList },
{ no: 2, name: 'push', kind: 'message', T: NotificationChannelPush },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): NotificationPreference {
return new NotificationPreference().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): NotificationPreference {
return new NotificationPreference().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): NotificationPreference {
return new NotificationPreference().fromJsonString(jsonString, options)
}
static equals(
a:
| NotificationPreference
| PlainMessage<NotificationPreference>
| undefined,
b:
| NotificationPreference
| PlainMessage<NotificationPreference>
| undefined,
): boolean {
return proto3.util.equals(NotificationPreference, a, b)
}
}
/**
* @generated from message bsky.ChatNotificationPreference
*/
export class ChatNotificationPreference extends Message<ChatNotificationPreference> {
/**
* @generated from field: bsky.ChatNotificationFilter filter = 1;
*/
filter = ChatNotificationFilter.UNSPECIFIED
/**
* @generated from field: bsky.NotificationChannelPush push = 2;
*/
push?: NotificationChannelPush
constructor(data?: PartialMessage<ChatNotificationPreference>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.ChatNotificationPreference'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{
no: 1,
name: 'filter',
kind: 'enum',
T: proto3.getEnumType(ChatNotificationFilter),
},
{ no: 2, name: 'push', kind: 'message', T: NotificationChannelPush },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): ChatNotificationPreference {
return new ChatNotificationPreference().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): ChatNotificationPreference {
return new ChatNotificationPreference().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): ChatNotificationPreference {
return new ChatNotificationPreference().fromJsonString(jsonString, options)
}
static equals(
a:
| ChatNotificationPreference
| PlainMessage<ChatNotificationPreference>
| undefined,
b:
| ChatNotificationPreference
| PlainMessage<ChatNotificationPreference>
| undefined,
): boolean {
return proto3.util.equals(ChatNotificationPreference, a, b)
}
}
/**
* @generated from message bsky.NotificationPreferences
*/
export class NotificationPreferences extends Message<NotificationPreferences> {
/**
* @generated from field: bsky.ChatNotificationPreference chat = 1;
*/
chat?: ChatNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference follow = 2;
*/
follow?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference like = 3;
*/
like?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference like_via_repost = 4;
*/
likeViaRepost?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference mention = 5;
*/
mention?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference quote = 6;
*/
quote?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference reply = 7;
*/
reply?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference repost = 8;
*/
repost?: FilterableNotificationPreference
/**
* @generated from field: bsky.FilterableNotificationPreference repost_via_repost = 9;
*/
repostViaRepost?: FilterableNotificationPreference
/**
* @generated from field: bsky.NotificationPreference starterpack_joined = 10;
*/
starterpackJoined?: NotificationPreference
/**
* @generated from field: bsky.NotificationPreference subscribed_post = 11;
*/
subscribedPost?: NotificationPreference
/**
* @generated from field: bsky.NotificationPreference unverified = 12;
*/
unverified?: NotificationPreference
/**
* @generated from field: bsky.NotificationPreference verified = 13;
*/
verified?: NotificationPreference
constructor(data?: PartialMessage<NotificationPreferences>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.NotificationPreferences'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'chat', kind: 'message', T: ChatNotificationPreference },
{
no: 2,
name: 'follow',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 3,
name: 'like',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 4,
name: 'like_via_repost',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 5,
name: 'mention',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 6,
name: 'quote',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 7,
name: 'reply',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 8,
name: 'repost',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 9,
name: 'repost_via_repost',
kind: 'message',
T: FilterableNotificationPreference,
},
{
no: 10,
name: 'starterpack_joined',
kind: 'message',
T: NotificationPreference,
},
{
no: 11,
name: 'subscribed_post',
kind: 'message',
T: NotificationPreference,
},
{ no: 12, name: 'unverified', kind: 'message', T: NotificationPreference },
{ no: 13, name: 'verified', kind: 'message', T: NotificationPreference },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): NotificationPreferences {
return new NotificationPreferences().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): NotificationPreferences {
return new NotificationPreferences().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): NotificationPreferences {
return new NotificationPreferences().fromJsonString(jsonString, options)
}
static equals(
a:
| NotificationPreferences
| PlainMessage<NotificationPreferences>
| undefined,
b:
| NotificationPreferences
| PlainMessage<NotificationPreferences>
| undefined,
): boolean {
return proto3.util.equals(NotificationPreferences, a, b)
}
}
/**
* @generated from message bsky.GetNotificationPreferencesResponse
*/
export class GetNotificationPreferencesResponse extends Message<GetNotificationPreferencesResponse> {
/**
* @generated from field: repeated bsky.NotificationPreferences preferences = 1;
*/
preferences: NotificationPreferences[] = []
constructor(data?: PartialMessage<GetNotificationPreferencesResponse>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsky.GetNotificationPreferencesResponse'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{
no: 1,
name: 'preferences',
kind: 'message',
T: NotificationPreferences,
repeated: true,
},
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>,
): GetNotificationPreferencesResponse {
return new GetNotificationPreferencesResponse().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>,
): GetNotificationPreferencesResponse {
return new GetNotificationPreferencesResponse().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>,
): GetNotificationPreferencesResponse {
return new GetNotificationPreferencesResponse().fromJsonString(
jsonString,
options,
)
}
static equals(
a:
| GetNotificationPreferencesResponse
| PlainMessage<GetNotificationPreferencesResponse>
| undefined,
b:
| GetNotificationPreferencesResponse
| PlainMessage<GetNotificationPreferencesResponse>
| undefined,
): boolean {
return proto3.util.equals(GetNotificationPreferencesResponse, a, b)
}
}
/**
* - list recent notifications for a user
* - notifications should include a uri for the record that caused the notif & a reason for the notification (reply, like, quotepost, etc)

View File

@ -736,14 +736,14 @@ export class Operation extends Message<Operation> {
actorDid = ''
/**
* @generated from field: string collection = 3;
* @generated from field: string namespace = 3;
*/
collection = ''
namespace = ''
/**
* @generated from field: string rkey = 4;
* @generated from field: string key = 4;
*/
rkey = ''
key = ''
/**
* @generated from field: bsync.Method method = 5;
@ -765,8 +765,8 @@ export class Operation extends Message<Operation> {
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'id', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 2, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 3, name: 'collection', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 4, name: 'rkey', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 3, name: 'namespace', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 4, name: 'key', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 5, name: 'method', kind: 'enum', T: proto3.getEnumType(Method) },
{ no: 6, name: 'payload', kind: 'scalar', T: 12 /* ScalarType.BYTES */ },
])
@ -805,19 +805,19 @@ export class Operation extends Message<Operation> {
*/
export class PutOperationRequest extends Message<PutOperationRequest> {
/**
* @generated from field: string collection = 1;
*/
collection = ''
/**
* @generated from field: string actor_did = 2;
* @generated from field: string actor_did = 1;
*/
actorDid = ''
/**
* @generated from field: string rkey = 3;
* @generated from field: string namespace = 2;
*/
rkey = ''
namespace = ''
/**
* @generated from field: string key = 3;
*/
key = ''
/**
* @generated from field: bsync.Method method = 4;
@ -837,9 +837,9 @@ export class PutOperationRequest extends Message<PutOperationRequest> {
static readonly runtime: typeof proto3 = proto3
static readonly typeName = 'bsync.PutOperationRequest'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'collection', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 2, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 3, name: 'rkey', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 2, name: 'namespace', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 3, name: 'key', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 4, name: 'method', kind: 'enum', T: proto3.getEnumType(Method) },
{ no: 5, name: 'payload', kind: 'scalar', T: 12 /* ScalarType.BYTES */ },
])

View File

@ -0,0 +1,75 @@
import { LexValue, stringifyLex } from '@atproto/lexicon'
import { BsyncClient } from './bsync'
import { lexicons } from './lexicon/lexicons'
import { Method } from './proto/bsync_pb'
export const createStashClient = (bsyncClient: BsyncClient): StashClient => {
return new StashClient(bsyncClient)
}
// An abstraction over the BsyncClient, that uses the bsync `PutOperation` RPC
// to store private data, which can be indexed by the dataplane and queried by the appview.
export class StashClient {
constructor(private readonly bsyncClient: BsyncClient) {}
create(input: CreateInput) {
this.validateLexicon(input.namespace, input.payload)
return this.putOperation(Method.CREATE, input)
}
update(input: UpdateInput) {
this.validateLexicon(input.namespace, input.payload)
return this.putOperation(Method.UPDATE, input)
}
delete(input: DeleteInput) {
return this.putOperation(Method.DELETE, { ...input, payload: undefined })
}
private validateLexicon(namespace: string, payload: LexValue) {
const result = lexicons.validate(namespace, payload)
if (!result.success) {
throw result.error
}
}
private async putOperation(method: Method, input: PutOperationInput) {
const { actorDid, namespace, key, payload } = input
await this.bsyncClient.putOperation({
actorDid,
namespace,
key,
method,
payload: payload
? Buffer.from(
stringifyLex({
$type: namespace,
...payload,
}),
)
: undefined,
})
}
}
type PutOperationInput = {
actorDid: string
namespace: string
key: string
payload: LexValue | undefined
}
type CreateInput = {
actorDid: string
namespace: string
key: string
payload: LexValue
}
type UpdateInput = CreateInput
type DeleteInput = {
actorDid: string
namespace: string
key: string
}

View File

@ -0,0 +1,156 @@
import { TestNetwork } from '@atproto/dev-env'
import { ProfileAssociatedChat } from '../dist/lexicon/types/app/bsky/actor/defs'
import { StashClient } from '../dist/stash'
type Database = TestNetwork['bsky']['db']
describe('private data', () => {
let network: TestNetwork
let stashClient: StashClient
let db: Database
const actorDid = 'did:plc:example'
// This lexicon has nothing special other than being simple, convenient to use in a test.
const namespace = 'app.bsky.actor.defs#profileAssociatedChat'
const key = 'self'
const validPayload0: ProfileAssociatedChat = { allowIncoming: 'all' }
const validPayload1: ProfileAssociatedChat = { allowIncoming: 'following' }
const invalidPayload: ProfileAssociatedChat = {
invalid: 'all',
} as unknown as ProfileAssociatedChat
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_private_data',
})
db = network.bsky.db
stashClient = network.bsky.ctx.stashClient
})
afterEach(async () => {
await clearPrivateData(db)
})
afterAll(async () => {
await network.close()
})
describe('create', () => {
it('creates entry', async () => {
await stashClient.create({
actorDid,
namespace,
key,
payload: validPayload0,
})
await network.processAll()
const dbResult = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', '=', actorDid)
.where('namespace', '=', namespace)
.where('key', '=', key)
.executeTakeFirstOrThrow()
expect(dbResult).toStrictEqual({
actorDid,
namespace,
key,
payload: JSON.stringify({ $type: namespace, ...validPayload0 }),
indexedAt: expect.any(String),
updatedAt: expect.any(String),
})
})
it('validates lexicon', async () => {
expect(() =>
stashClient.create({
actorDid,
namespace,
key,
payload: invalidPayload,
}),
).toThrow('Object must have the property "allowIncoming"')
})
})
describe('update', () => {
it('updates entry', async () => {
await stashClient.create({
actorDid,
namespace,
key,
payload: validPayload0,
})
await network.processAll()
await stashClient.update({
actorDid,
namespace,
key,
payload: validPayload1,
})
await network.processAll()
const dbResult = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', '=', actorDid)
.where('namespace', '=', namespace)
.where('key', '=', key)
.executeTakeFirstOrThrow()
expect(dbResult).toStrictEqual({
actorDid,
namespace,
key,
payload: JSON.stringify({ $type: namespace, ...validPayload1 }),
indexedAt: expect.any(String),
updatedAt: expect.any(String),
})
})
it('validates lexicon', async () => {
expect(() =>
stashClient.update({
actorDid,
namespace,
key,
payload: invalidPayload,
}),
).toThrow('Object must have the property "allowIncoming"')
})
})
describe('delete', () => {
it('deletes entry', async () => {
await stashClient.create({
actorDid,
namespace,
key,
payload: validPayload0,
})
await network.processAll()
await stashClient.delete({
actorDid,
namespace,
key,
})
await network.processAll()
const dbResult = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', '=', actorDid)
.where('namespace', '=', namespace)
.where('key', '=', key)
.executeTakeFirst()
expect(dbResult).toBe(undefined)
})
})
})
const clearPrivateData = async (db: Database) => {
await db.db.deleteFrom('private_data').execute()
}

View File

@ -2,11 +2,22 @@ import { AtpAgent } from '@atproto/api'
import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
import { delayCursor } from '../../src/api/app/bsky/notification/listNotifications'
import { ids } from '../../src/lexicon/lexicons'
import {
ChatPreference,
FilterablePreference,
Preference,
Preferences,
} from '../../src/lexicon/types/app/bsky/notification/defs'
import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications'
import { InputSchema } from '../../src/lexicon/types/app/bsky/notification/putPreferencesV2'
import { forSnapshot, paginateAll } from '../_util'
type Database = TestNetwork['bsky']['db']
describe('notification views', () => {
let network: TestNetwork
let db: Database
let agent: AtpAgent
let sc: SeedClient
@ -19,6 +30,7 @@ describe('notification views', () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_notifications',
})
db = network.bsky.db
agent = network.bsky.getClient()
sc = network.getSeedClient()
await basicSeed(sc)
@ -905,4 +917,213 @@ describe('notification views', () => {
})
})
})
describe('preferences v2', () => {
beforeEach(async () => {
await clearPrivateData(db)
})
// Defaults
const fp: FilterablePreference = {
filter: 'all',
list: true,
push: true,
}
const p: Preference = {
list: true,
push: true,
}
const cp: ChatPreference = {
filter: 'all',
push: true,
}
it('gets preferences filling up with the defaults', async () => {
const actorDid = sc.dids.carol
const getAndAssert = async (
expectedApi: Preferences,
expectedDb: Preferences | undefined,
) => {
const { data } = await agent.app.bsky.notification.getPreferences(
{},
{
headers: await network.serviceHeaders(
actorDid,
ids.AppBskyNotificationGetPreferences,
),
},
)
expect(data.preferences).toStrictEqual(expectedApi)
const dbResult = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', '=', actorDid)
.where('namespace', '=', 'app.bsky.notification.defs#preferences')
.where('key', '=', 'self')
.executeTakeFirst()
if (dbResult === undefined) {
expect(dbResult).toBe(expectedDb)
} else {
expect(dbResult).toStrictEqual({
actorDid: actorDid,
namespace: 'app.bsky.notification.defs#preferences',
key: 'self',
indexedAt: expect.any(String),
payload: expect.anything(), // Better to compare payload parsed.
updatedAt: expect.any(String),
})
expect(JSON.parse(dbResult.payload)).toStrictEqual({
$type: 'app.bsky.notification.defs#preferences',
...expectedDb,
})
}
}
const expectedApi0: Preferences = {
chat: cp,
follow: fp,
like: fp,
likeViaRepost: fp,
mention: fp,
quote: fp,
reply: fp,
repost: fp,
repostViaRepost: fp,
starterpackJoined: p,
subscribedPost: p,
unverified: p,
verified: p,
}
// The user has no preferences set yet, so nothing stored.
const expectedDb0 = undefined
await getAndAssert(expectedApi0, expectedDb0)
await agent.app.bsky.notification.putPreferencesV2(
{ verified: { list: false, push: false } },
{
encoding: 'application/json',
headers: await network.serviceHeaders(
actorDid,
ids.AppBskyNotificationPutPreferencesV2,
),
},
)
await network.processAll()
const expectedApi1: Preferences = {
chat: cp,
follow: fp,
like: fp,
likeViaRepost: fp,
mention: fp,
quote: fp,
reply: fp,
repost: fp,
repostViaRepost: fp,
starterpackJoined: p,
subscribedPost: p,
unverified: p,
verified: { list: false, push: false },
}
// Stored all the defaults.
const expectedDb1 = expectedApi1
await getAndAssert(expectedApi1, expectedDb1)
})
it('stores the preferences setting the defaults', async () => {
const actorDid = sc.dids.carol
const putAndAssert = async (
input: InputSchema,
expected: Preferences,
) => {
const { data } = await agent.app.bsky.notification.putPreferencesV2(
input,
{
encoding: 'application/json',
headers: await network.serviceHeaders(
actorDid,
ids.AppBskyNotificationPutPreferencesV2,
),
},
)
await network.processAll()
expect(data.preferences).toStrictEqual(expected)
const dbResult = await db.db
.selectFrom('private_data')
.selectAll()
.where('actorDid', '=', actorDid)
.where('namespace', '=', 'app.bsky.notification.defs#preferences')
.where('key', '=', 'self')
.executeTakeFirstOrThrow()
expect(dbResult).toStrictEqual({
actorDid: actorDid,
namespace: 'app.bsky.notification.defs#preferences',
key: 'self',
indexedAt: expect.any(String),
payload: expect.anything(), // Better to compare payload parsed.
updatedAt: expect.any(String),
})
expect(JSON.parse(dbResult.payload)).toStrictEqual({
$type: 'app.bsky.notification.defs#preferences',
...expected,
})
}
const input0 = {
chat: {
push: false,
filter: 'accepted',
},
}
const expected0: Preferences = {
chat: input0.chat,
follow: fp,
like: fp,
likeViaRepost: fp,
mention: fp,
quote: fp,
reply: fp,
repost: fp,
repostViaRepost: fp,
starterpackJoined: p,
subscribedPost: p,
unverified: p,
verified: p,
}
await putAndAssert(input0, expected0)
const input1 = {
mention: {
list: false,
push: false,
filter: 'follows',
},
}
const expected1: Preferences = {
// Kept from the previous call.
chat: input0.chat,
follow: fp,
like: fp,
likeViaRepost: fp,
mention: input1.mention,
quote: fp,
reply: fp,
repost: fp,
repostViaRepost: fp,
starterpackJoined: p,
subscribedPost: p,
unverified: p,
verified: p,
}
await putAndAssert(input1, expected1)
})
})
})
const clearPrivateData = async (db: Database) => {
await db.db.deleteFrom('private_data').execute()
}

View File

@ -82,6 +82,17 @@ const validateOp = (req: PutOperationRequest): Operation => {
throw new ConnectError('operation method is invalid', Code.InvalidArgument)
}
if (req.method === Method.CREATE || req.method === Method.UPDATE) {
try {
JSON.parse(new TextDecoder().decode(req.payload))
} catch (error) {
throw new ConnectError(
'payload must be a valid JSON when method is CREATE or UPDATE',
Code.InvalidArgument,
)
}
}
if (req.method === Method.DELETE && req.payload.length > 0) {
throw new ConnectError(
'cannot specify a payload when method is DELETE',

View File

@ -16,6 +16,10 @@ describe('operations', () => {
let bsync: BsyncService
let client: BsyncClient
const validPayload0 = Buffer.from(JSON.stringify({ value: 0 }))
const validPayload1 = Buffer.from(JSON.stringify({ value: 1 }))
const invalidPayload = Buffer.from('{invalid json}')
beforeAll(async () => {
bsync = await BsyncService.create(
envToCfg({
@ -55,7 +59,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
await expect(tryPutOperation1).rejects.toEqual(
new ConnectError('missing auth', Code.Unauthenticated),
@ -71,7 +75,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
await expect(tryPutOperation2).rejects.toEqual(
new ConnectError('invalid api key', Code.Unauthenticated),
@ -85,7 +89,7 @@ describe('operations', () => {
namespace: 'bad-namespace',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([]),
payload: validPayload0,
}),
).rejects.toEqual(
new ConnectError(
@ -99,7 +103,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([]),
payload: validPayload0,
}),
).rejects.toEqual(
new ConnectError(
@ -113,7 +117,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: '',
method: Method.CREATE,
payload: Buffer.from([]),
payload: validPayload0,
}),
).rejects.toEqual(
new ConnectError('operation key is required', Code.InvalidArgument),
@ -124,18 +128,46 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.UNSPECIFIED,
payload: Buffer.from([]),
payload: validPayload0,
}),
).rejects.toEqual(
new ConnectError('operation method is invalid', Code.InvalidArgument),
)
await expect(
client.putOperation({
actorDid: 'did:example:a',
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: invalidPayload,
}),
).rejects.toEqual(
new ConnectError(
'payload must be a valid JSON when method is CREATE or UPDATE',
Code.InvalidArgument,
),
)
await expect(
client.putOperation({
actorDid: 'did:example:a',
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.UPDATE,
payload: invalidPayload,
}),
).rejects.toEqual(
new ConnectError(
'payload must be a valid JSON when method is CREATE or UPDATE',
Code.InvalidArgument,
),
)
await expect(
client.putOperation({
actorDid: 'did:example:a',
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.DELETE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
}),
).rejects.toEqual(
new ConnectError(
@ -151,14 +183,14 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
const res2 = await client.putOperation({
actorDid: 'did:example:a',
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.UPDATE,
payload: Buffer.from([4, 5, 6]),
payload: validPayload1,
})
expect(res1.operation?.id).toBe('1')
@ -170,7 +202,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
createdAt: expect.any(Date),
},
{
@ -179,7 +211,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.UPDATE,
payload: Buffer.from([4, 5, 6]),
payload: validPayload1,
createdAt: expect.any(Date),
},
])
@ -191,7 +223,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
const op = res.operation
@ -202,7 +234,7 @@ describe('operations', () => {
expect(op.namespace).toBe('app.bsky.some.col')
expect(op.key).toBe('key1')
expect(op.method).toBe(Method.CREATE)
expect(op.payload).toEqual(Uint8Array.from([1, 2, 3]))
expect(op.payload).toEqual(new Uint8Array(validPayload0))
})
})
@ -237,7 +269,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
}
@ -266,7 +298,7 @@ describe('operations', () => {
namespace: 'app.bsky.some.col',
key: 'key1',
method: Method.CREATE,
payload: Buffer.from([1, 2, 3]),
payload: validPayload0,
})
const res = await scanPromise
expect(res.operations.length).toEqual(1)

View File

@ -138,9 +138,11 @@ import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'
import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'
import * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'
import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'
import * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'
import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'
import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'
import * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'
import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
import * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'
@ -1903,6 +1905,17 @@ export class AppBskyNotificationNS {
this._server = server
}
getPreferences<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationGetPreferences.Handler<ExtractAuth<AV>>,
AppBskyNotificationGetPreferences.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getUnreadCount<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
@ -1936,6 +1949,17 @@ export class AppBskyNotificationNS {
return this._server.xrpc.method(nsid, cfg)
}
putPreferencesV2<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationPutPreferencesV2.Handler<ExtractAuth<AV>>,
AppBskyNotificationPutPreferencesV2.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
registerPush<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,

View File

@ -9732,6 +9732,147 @@ export const schemaDict = {
type: 'object',
properties: {},
},
chatPreference: {
type: 'object',
required: ['filter', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'accepted'],
},
push: {
type: 'boolean',
},
},
},
filterablePreference: {
type: 'object',
required: ['filter', 'list', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'follows'],
},
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preference: {
type: 'object',
required: ['list', 'push'],
properties: {
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preferences: {
type: 'object',
required: [
'chat',
'follow',
'like',
'likeViaRepost',
'mention',
'quote',
'reply',
'repost',
'repostViaRepost',
'starterpackJoined',
'subscribedPost',
'unverified',
'verified',
],
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
},
AppBskyNotificationGetPreferences: {
lexicon: 1,
id: 'app.bsky.notification.getPreferences',
defs: {
main: {
type: 'query',
description:
'Get notification-related preferences for an account. Requires auth.',
parameters: {
type: 'params',
properties: {},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationGetUnreadCount: {
@ -9924,6 +10065,90 @@ export const schemaDict = {
},
},
},
AppBskyNotificationPutPreferencesV2: {
lexicon: 1,
id: 'app.bsky.notification.putPreferencesV2',
defs: {
main: {
type: 'procedure',
description:
'Set notification-related preferences for an account. Requires auth.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationRegisterPush: {
lexicon: 1,
id: 'app.bsky.notification.registerPush',
@ -16557,10 +16782,12 @@ export const ids = {
AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
AppBskyLabelerService: 'app.bsky.labeler.service',
AppBskyNotificationDefs: 'app.bsky.notification.defs',
AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',
AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',
AppBskyNotificationListNotifications:
'app.bsky.notification.listNotifications',
AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',
AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',
AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',
AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',
AppBskyRichtextFacet: 'app.bsky.richtext.facet',

View File

@ -27,3 +27,79 @@ export function isRecordDeleted<V>(v: V) {
export function validateRecordDeleted<V>(v: V) {
return validate<RecordDeleted & V>(v, id, hashRecordDeleted)
}
export interface ChatPreference {
$type?: 'app.bsky.notification.defs#chatPreference'
filter: 'all' | 'accepted' | (string & {})
push: boolean
}
const hashChatPreference = 'chatPreference'
export function isChatPreference<V>(v: V) {
return is$typed(v, id, hashChatPreference)
}
export function validateChatPreference<V>(v: V) {
return validate<ChatPreference & V>(v, id, hashChatPreference)
}
export interface FilterablePreference {
$type?: 'app.bsky.notification.defs#filterablePreference'
filter: 'all' | 'follows' | (string & {})
list: boolean
push: boolean
}
const hashFilterablePreference = 'filterablePreference'
export function isFilterablePreference<V>(v: V) {
return is$typed(v, id, hashFilterablePreference)
}
export function validateFilterablePreference<V>(v: V) {
return validate<FilterablePreference & V>(v, id, hashFilterablePreference)
}
export interface Preference {
$type?: 'app.bsky.notification.defs#preference'
list: boolean
push: boolean
}
const hashPreference = 'preference'
export function isPreference<V>(v: V) {
return is$typed(v, id, hashPreference)
}
export function validatePreference<V>(v: V) {
return validate<Preference & V>(v, id, hashPreference)
}
export interface Preferences {
$type?: 'app.bsky.notification.defs#preferences'
chat: ChatPreference
follow: FilterablePreference
like: FilterablePreference
likeViaRepost: FilterablePreference
mention: FilterablePreference
quote: FilterablePreference
reply: FilterablePreference
repost: FilterablePreference
repostViaRepost: FilterablePreference
starterpackJoined: Preference
subscribedPost: Preference
unverified: Preference
verified: Preference
}
const hashPreferences = 'preferences'
export function isPreferences<V>(v: V) {
return is$typed(v, id, hashPreferences)
}
export function validatePreferences<V>(v: V) {
return validate<Preferences & V>(v, id, hashPreferences)
}

View File

@ -0,0 +1,52 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.getPreferences'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
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 | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput

View File

@ -0,0 +1,69 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.putPreferencesV2'
export interface QueryParams {}
export interface InputSchema {
chat?: AppBskyNotificationDefs.ChatPreference
follow?: AppBskyNotificationDefs.FilterablePreference
like?: AppBskyNotificationDefs.FilterablePreference
likeViaRepost?: AppBskyNotificationDefs.FilterablePreference
mention?: AppBskyNotificationDefs.FilterablePreference
quote?: AppBskyNotificationDefs.FilterablePreference
reply?: AppBskyNotificationDefs.FilterablePreference
repost?: AppBskyNotificationDefs.FilterablePreference
repostViaRepost?: AppBskyNotificationDefs.FilterablePreference
starterpackJoined?: AppBskyNotificationDefs.Preference
subscribedPost?: AppBskyNotificationDefs.Preference
unverified?: AppBskyNotificationDefs.Preference
verified?: AppBskyNotificationDefs.Preference
}
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
headers?: { [key: string]: string }
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput

View File

@ -138,9 +138,11 @@ import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor.js'
import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList.js'
import * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread.js'
import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices.js'
import * as AppBskyNotificationGetPreferences from './types/app/bsky/notification/getPreferences.js'
import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount.js'
import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications.js'
import * as AppBskyNotificationPutPreferences from './types/app/bsky/notification/putPreferences.js'
import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
import * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'
@ -1903,6 +1905,17 @@ export class AppBskyNotificationNS {
this._server = server
}
getPreferences<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationGetPreferences.Handler<ExtractAuth<AV>>,
AppBskyNotificationGetPreferences.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.getPreferences' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getUnreadCount<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
@ -1936,6 +1949,17 @@ export class AppBskyNotificationNS {
return this._server.xrpc.method(nsid, cfg)
}
putPreferencesV2<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyNotificationPutPreferencesV2.Handler<ExtractAuth<AV>>,
AppBskyNotificationPutPreferencesV2.HandlerReqCtx<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.notification.putPreferencesV2' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
registerPush<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,

View File

@ -9732,6 +9732,147 @@ export const schemaDict = {
type: 'object',
properties: {},
},
chatPreference: {
type: 'object',
required: ['filter', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'accepted'],
},
push: {
type: 'boolean',
},
},
},
filterablePreference: {
type: 'object',
required: ['filter', 'list', 'push'],
properties: {
filter: {
type: 'string',
knownValues: ['all', 'follows'],
},
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preference: {
type: 'object',
required: ['list', 'push'],
properties: {
list: {
type: 'boolean',
},
push: {
type: 'boolean',
},
},
},
preferences: {
type: 'object',
required: [
'chat',
'follow',
'like',
'likeViaRepost',
'mention',
'quote',
'reply',
'repost',
'repostViaRepost',
'starterpackJoined',
'subscribedPost',
'unverified',
'verified',
],
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
},
AppBskyNotificationGetPreferences: {
lexicon: 1,
id: 'app.bsky.notification.getPreferences',
defs: {
main: {
type: 'query',
description:
'Get notification-related preferences for an account. Requires auth.',
parameters: {
type: 'params',
properties: {},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationGetUnreadCount: {
@ -9924,6 +10065,90 @@ export const schemaDict = {
},
},
},
AppBskyNotificationPutPreferencesV2: {
lexicon: 1,
id: 'app.bsky.notification.putPreferencesV2',
defs: {
main: {
type: 'procedure',
description:
'Set notification-related preferences for an account. Requires auth.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
properties: {
chat: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#chatPreference',
},
follow: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
like: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
likeViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
mention: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
quote: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
reply: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
repostViaRepost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#filterablePreference',
},
starterpackJoined: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
subscribedPost: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
unverified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
verified: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preference',
},
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.notification.defs#preferences',
},
},
},
},
},
},
},
AppBskyNotificationRegisterPush: {
lexicon: 1,
id: 'app.bsky.notification.registerPush',
@ -16557,10 +16782,12 @@ export const ids = {
AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
AppBskyLabelerService: 'app.bsky.labeler.service',
AppBskyNotificationDefs: 'app.bsky.notification.defs',
AppBskyNotificationGetPreferences: 'app.bsky.notification.getPreferences',
AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount',
AppBskyNotificationListNotifications:
'app.bsky.notification.listNotifications',
AppBskyNotificationPutPreferences: 'app.bsky.notification.putPreferences',
AppBskyNotificationPutPreferencesV2: 'app.bsky.notification.putPreferencesV2',
AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush',
AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen',
AppBskyRichtextFacet: 'app.bsky.richtext.facet',

View File

@ -27,3 +27,79 @@ export function isRecordDeleted<V>(v: V) {
export function validateRecordDeleted<V>(v: V) {
return validate<RecordDeleted & V>(v, id, hashRecordDeleted)
}
export interface ChatPreference {
$type?: 'app.bsky.notification.defs#chatPreference'
filter: 'all' | 'accepted' | (string & {})
push: boolean
}
const hashChatPreference = 'chatPreference'
export function isChatPreference<V>(v: V) {
return is$typed(v, id, hashChatPreference)
}
export function validateChatPreference<V>(v: V) {
return validate<ChatPreference & V>(v, id, hashChatPreference)
}
export interface FilterablePreference {
$type?: 'app.bsky.notification.defs#filterablePreference'
filter: 'all' | 'follows' | (string & {})
list: boolean
push: boolean
}
const hashFilterablePreference = 'filterablePreference'
export function isFilterablePreference<V>(v: V) {
return is$typed(v, id, hashFilterablePreference)
}
export function validateFilterablePreference<V>(v: V) {
return validate<FilterablePreference & V>(v, id, hashFilterablePreference)
}
export interface Preference {
$type?: 'app.bsky.notification.defs#preference'
list: boolean
push: boolean
}
const hashPreference = 'preference'
export function isPreference<V>(v: V) {
return is$typed(v, id, hashPreference)
}
export function validatePreference<V>(v: V) {
return validate<Preference & V>(v, id, hashPreference)
}
export interface Preferences {
$type?: 'app.bsky.notification.defs#preferences'
chat: ChatPreference
follow: FilterablePreference
like: FilterablePreference
likeViaRepost: FilterablePreference
mention: FilterablePreference
quote: FilterablePreference
reply: FilterablePreference
repost: FilterablePreference
repostViaRepost: FilterablePreference
starterpackJoined: Preference
subscribedPost: Preference
unverified: Preference
verified: Preference
}
const hashPreferences = 'preferences'
export function isPreferences<V>(v: V) {
return is$typed(v, id, hashPreferences)
}
export function validatePreferences<V>(v: V) {
return validate<Preferences & V>(v, id, hashPreferences)
}

View File

@ -0,0 +1,52 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.getPreferences'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
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 | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput

View File

@ -0,0 +1,69 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { type ValidationResult, BlobRef } from '@atproto/lexicon'
import { CID } from 'multiformats/cid'
import { validate as _validate } from '../../../../lexicons'
import {
type $Typed,
is$typed as _is$typed,
type OmitKey,
} from '../../../../util'
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
import type * as AppBskyNotificationDefs from './defs.js'
const is$typed = _is$typed,
validate = _validate
const id = 'app.bsky.notification.putPreferencesV2'
export interface QueryParams {}
export interface InputSchema {
chat?: AppBskyNotificationDefs.ChatPreference
follow?: AppBskyNotificationDefs.FilterablePreference
like?: AppBskyNotificationDefs.FilterablePreference
likeViaRepost?: AppBskyNotificationDefs.FilterablePreference
mention?: AppBskyNotificationDefs.FilterablePreference
quote?: AppBskyNotificationDefs.FilterablePreference
reply?: AppBskyNotificationDefs.FilterablePreference
repost?: AppBskyNotificationDefs.FilterablePreference
repostViaRepost?: AppBskyNotificationDefs.FilterablePreference
starterpackJoined?: AppBskyNotificationDefs.Preference
subscribedPost?: AppBskyNotificationDefs.Preference
unverified?: AppBskyNotificationDefs.Preference
verified?: AppBskyNotificationDefs.Preference
}
export interface OutputSchema {
preferences: AppBskyNotificationDefs.Preferences
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
headers?: { [key: string]: string }
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
resetRouteRateLimits: () => Promise<void>
}
export type Handler<HA extends HandlerAuth = never> = (
ctx: HandlerReqCtx<HA>,
) => Promise<HandlerOutput> | HandlerOutput