a3ce23c4cc
* Add hotness as a thread sorting option * Changeset
3345 lines
93 KiB
TypeScript
3345 lines
93 KiB
TypeScript
import { TestNetworkNoAppView } from '@atproto/dev-env'
|
||
import { TID } from '@atproto/common-web'
|
||
import {
|
||
AppBskyActorDefs,
|
||
AppBskyActorProfile,
|
||
AtpAgent,
|
||
ComAtprotoRepoPutRecord,
|
||
DEFAULT_LABEL_SETTINGS,
|
||
} from '../src'
|
||
import {
|
||
getSavedFeedType,
|
||
savedFeedsToUriArrays,
|
||
validateSavedFeed,
|
||
} from '../src/util'
|
||
|
||
describe('agent', () => {
|
||
let network: TestNetworkNoAppView
|
||
|
||
beforeAll(async () => {
|
||
network = await TestNetworkNoAppView.create({
|
||
dbPostgresSchema: 'api_atp_agent',
|
||
})
|
||
})
|
||
|
||
afterAll(async () => {
|
||
await network.close()
|
||
})
|
||
|
||
const getProfileDisplayName = async (
|
||
agent: AtpAgent,
|
||
): Promise<string | undefined> => {
|
||
try {
|
||
const res = await agent.app.bsky.actor.profile.get({
|
||
repo: agent.accountDid,
|
||
rkey: 'self',
|
||
})
|
||
return res.value.displayName ?? ''
|
||
} catch (err) {
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
it('clones correctly', () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
const agent2 = agent.clone()
|
||
expect(agent2 instanceof AtpAgent).toBeTruthy()
|
||
expect(agent.service).toEqual(agent2.service)
|
||
})
|
||
|
||
it('upsertProfile correctly creates and updates profiles.', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
|
||
await agent.createAccount({
|
||
handle: 'user1.test',
|
||
email: 'user1@test.com',
|
||
password: 'password',
|
||
})
|
||
const displayName1 = await getProfileDisplayName(agent)
|
||
expect(displayName1).toBeFalsy()
|
||
|
||
await agent.upsertProfile((existing) => {
|
||
expect(existing).toBeFalsy()
|
||
return {
|
||
displayName: 'Bob',
|
||
}
|
||
})
|
||
|
||
const displayName2 = await getProfileDisplayName(agent)
|
||
expect(displayName2).toBe('Bob')
|
||
|
||
await agent.upsertProfile((existing) => {
|
||
expect(existing).toBeTruthy()
|
||
return {
|
||
displayName: existing?.displayName?.toUpperCase(),
|
||
}
|
||
})
|
||
|
||
const displayName3 = await getProfileDisplayName(agent)
|
||
expect(displayName3).toBe('BOB')
|
||
})
|
||
|
||
it('upsertProfile correctly handles CAS failures.', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user2.test',
|
||
email: 'user2@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
const displayName1 = await getProfileDisplayName(agent)
|
||
expect(displayName1).toBeFalsy()
|
||
|
||
let hasConflicted = false
|
||
let ranTwice = false
|
||
await agent.upsertProfile(async (_existing) => {
|
||
if (!hasConflicted) {
|
||
await agent.com.atproto.repo.putRecord({
|
||
repo: agent.accountDid,
|
||
collection: 'app.bsky.actor.profile',
|
||
rkey: 'self',
|
||
record: {
|
||
$type: 'app.bsky.actor.profile',
|
||
displayName: String(Math.random()),
|
||
},
|
||
})
|
||
hasConflicted = true
|
||
} else {
|
||
ranTwice = true
|
||
}
|
||
return {
|
||
displayName: 'Bob',
|
||
}
|
||
})
|
||
expect(ranTwice).toBe(true)
|
||
|
||
const displayName2 = await getProfileDisplayName(agent)
|
||
expect(displayName2).toBe('Bob')
|
||
})
|
||
|
||
it('upsertProfile wont endlessly retry CAS failures.', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user3.test',
|
||
email: 'user3@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
const displayName1 = await getProfileDisplayName(agent)
|
||
expect(displayName1).toBeFalsy()
|
||
|
||
const p = agent.upsertProfile(async (_existing) => {
|
||
await agent.com.atproto.repo.putRecord({
|
||
repo: agent.accountDid,
|
||
collection: 'app.bsky.actor.profile',
|
||
rkey: 'self',
|
||
record: {
|
||
$type: 'app.bsky.actor.profile',
|
||
displayName: String(Math.random()),
|
||
},
|
||
})
|
||
return {
|
||
displayName: 'Bob',
|
||
}
|
||
})
|
||
await expect(p).rejects.toThrow(ComAtprotoRepoPutRecord.InvalidSwapError)
|
||
})
|
||
|
||
it('upsertProfile validates the record.', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user4.test',
|
||
email: 'user4@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
const p = agent.upsertProfile((_existing) => {
|
||
return {
|
||
displayName: { string: 'Bob' },
|
||
} as unknown as AppBskyActorProfile.Record
|
||
})
|
||
await expect(p).rejects.toThrow('Record/displayName must be a string')
|
||
})
|
||
|
||
describe('app', () => {
|
||
it('should retrieve the api app', () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
expect(agent.api).toBe(agent)
|
||
expect(agent.app).toBeDefined()
|
||
})
|
||
})
|
||
|
||
describe('post', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.post({ text: 'foo' })).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('deletePost', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.deletePost('foo')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('like', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.like('foo', 'bar')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('deleteLike', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.deleteLike('foo')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('repost', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.repost('foo', 'bar')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('deleteRepost', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.deleteRepost('foo')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('follow', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.follow('foo')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('deleteFollow', () => {
|
||
it('should throw if no session', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in')
|
||
})
|
||
})
|
||
|
||
describe('preferences methods', () => {
|
||
it('gets and sets preferences correctly', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user5.test',
|
||
email: 'user5@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
const DEFAULT_LABELERS = AtpAgent.appLabelers.map((did) => ({
|
||
did,
|
||
labels: {},
|
||
}))
|
||
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: { pinned: undefined, saved: undefined },
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: DEFAULT_LABEL_SETTINGS,
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setAdultContentEnabled(true)
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: { pinned: undefined, saved: undefined },
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: true,
|
||
labels: DEFAULT_LABEL_SETTINGS,
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setAdultContentEnabled(false)
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: { pinned: undefined, saved: undefined },
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: DEFAULT_LABEL_SETTINGS,
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setContentLabelPref('misinfo', 'hide')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: { pinned: undefined, saved: undefined },
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: { ...DEFAULT_LABEL_SETTINGS, misinfo: 'hide' },
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setContentLabelPref('spam', 'ignore')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: { pinned: undefined, saved: undefined },
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [
|
||
'at://bob.com/app.bsky.feed.generator/fake',
|
||
'at://bob.com/app.bsky.feed.generator/fake2',
|
||
],
|
||
saved: [
|
||
'at://bob.com/app.bsky.feed.generator/fake',
|
||
'at://bob.com/app.bsky.feed.generator/fake2',
|
||
],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: undefined,
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setFeedViewPrefs('home', { hideReplies: true })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setFeedViewPrefs('home', { hideReplies: false })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setFeedViewPrefs('other', { hideReplies: true })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
other: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'hotness',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setThreadViewPrefs({ sort: 'random' })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
other: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'random',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setThreadViewPrefs({ sort: 'oldest' })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
other: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setInterestsPref({ tags: ['foo', 'bar'] })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
misinfo: 'hide',
|
||
spam: 'ignore',
|
||
},
|
||
labelers: DEFAULT_LABELERS,
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
other: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: ['foo', 'bar'],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: [],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
})
|
||
|
||
it('resolves duplicates correctly', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
|
||
await agent.createAccount({
|
||
handle: 'user6.test',
|
||
email: 'user6@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'porn',
|
||
visibility: 'show',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'porn',
|
||
visibility: 'hide',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'porn',
|
||
visibility: 'show',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'porn',
|
||
visibility: 'warn',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#labelersPref',
|
||
labelers: [
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#labelersPref',
|
||
labelers: [
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
},
|
||
{
|
||
did: 'did:plc:other',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#adultContentPref',
|
||
enabled: true,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#adultContentPref',
|
||
enabled: false,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#adultContentPref',
|
||
enabled: true,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [
|
||
'at://bob.com/app.bsky.feed.generator/fake',
|
||
'at://bob.com/app.bsky.feed.generator/fake2',
|
||
],
|
||
saved: [
|
||
'at://bob.com/app.bsky.feed.generator/fake',
|
||
'at://bob.com/app.bsky.feed.generator/fake2',
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#personalDetailsPref',
|
||
birthDate: '2023-09-11T18:05:42.556Z',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#personalDetailsPref',
|
||
birthDate: '2021-09-11T18:05:42.556Z',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#feedViewPref',
|
||
feed: 'home',
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#feedViewPref',
|
||
feed: 'home',
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#threadViewPref',
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#threadViewPref',
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#bskyAppStatePref',
|
||
queuedNudges: ['one'],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#bskyAppStatePref',
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
},
|
||
],
|
||
})
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: true,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
porn: 'warn',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
{
|
||
did: 'did:plc:other',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2021-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setAdultContentEnabled(false)
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
porn: 'warn',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
{
|
||
did: 'did:plc:other',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2021-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setContentLabelPref('porn', 'ignore')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
nsfw: 'ignore',
|
||
porn: 'ignore',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
{
|
||
did: 'did:plc:other',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2021-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.removeLabeler('did:plc:other')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
],
|
||
feeds: {
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
nsfw: 'ignore',
|
||
porn: 'ignore',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2021-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
nsfw: 'ignore',
|
||
porn: 'ignore',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2021-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
nsfw: 'ignore',
|
||
porn: 'ignore',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: true,
|
||
hideRepliesByUnfollowed: false,
|
||
hideRepliesByLikeCount: 10,
|
||
hideReposts: true,
|
||
hideQuotePosts: true,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'newest',
|
||
prioritizeFollowedUsers: false,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
await agent.setFeedViewPrefs('home', {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
})
|
||
await agent.setThreadViewPrefs({
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
})
|
||
await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
|
||
await agent.bskyAppQueueNudges('three')
|
||
await expect(agent.getPreferences()).resolves.toStrictEqual({
|
||
feeds: {
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
savedFeeds: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
moderationPrefs: {
|
||
adultContentEnabled: false,
|
||
labels: {
|
||
...DEFAULT_LABEL_SETTINGS,
|
||
nsfw: 'ignore',
|
||
porn: 'ignore',
|
||
},
|
||
labelers: [
|
||
...AtpAgent.appLabelers.map((did) => ({ did, labels: {} })),
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
labels: {},
|
||
},
|
||
],
|
||
mutedWords: [],
|
||
hiddenPosts: [],
|
||
},
|
||
birthDate: new Date('2023-09-11T18:05:42.556Z'),
|
||
feedViewPrefs: {
|
||
home: {
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
},
|
||
threadViewPrefs: {
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
interests: {
|
||
tags: [],
|
||
},
|
||
bskyAppState: {
|
||
activeProgressGuide: undefined,
|
||
queuedNudges: ['two', 'three'],
|
||
nuxs: [],
|
||
},
|
||
})
|
||
|
||
const res = await agent.app.bsky.actor.getPreferences()
|
||
expect(res.data.preferences.sort(byType)).toStrictEqual(
|
||
[
|
||
{
|
||
$type: 'app.bsky.actor.defs#bskyAppStatePref',
|
||
queuedNudges: ['two', 'three'],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#adultContentPref',
|
||
enabled: false,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'porn',
|
||
visibility: 'ignore',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#contentLabelPref',
|
||
label: 'nsfw',
|
||
visibility: 'ignore',
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#labelersPref',
|
||
labelers: [
|
||
{
|
||
did: 'did:plc:first-labeler',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
saved: ['at://bob.com/app.bsky.feed.generator/fake'],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPrefV2',
|
||
items: [
|
||
{
|
||
id: expect.any(String),
|
||
pinned: true,
|
||
type: 'timeline',
|
||
value: 'following',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#personalDetailsPref',
|
||
birthDate: '2023-09-11T18:05:42.556Z',
|
||
},
|
||
|
||
{
|
||
$type: 'app.bsky.actor.defs#feedViewPref',
|
||
feed: 'home',
|
||
hideReplies: false,
|
||
hideRepliesByUnfollowed: true,
|
||
hideRepliesByLikeCount: 0,
|
||
hideReposts: false,
|
||
hideQuotePosts: false,
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#threadViewPref',
|
||
sort: 'oldest',
|
||
prioritizeFollowedUsers: true,
|
||
},
|
||
].sort(byType),
|
||
)
|
||
})
|
||
|
||
describe('muted words', () => {
|
||
let agent: AtpAgent
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user7.test',
|
||
email: 'user7@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
afterEach(async () => {
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
await agent.removeMutedWords(moderationPrefs.mutedWords)
|
||
})
|
||
|
||
describe('addMutedWord', () => {
|
||
it('inserts', async () => {
|
||
const expiresAt = new Date(Date.now() + 6e3).toISOString()
|
||
await agent.addMutedWord({
|
||
value: 'word',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
expiresAt,
|
||
})
|
||
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
const word = moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'word',
|
||
)
|
||
|
||
expect(word!.id).toBeTruthy()
|
||
expect(word!.targets).toEqual(['content'])
|
||
expect(word!.actorTarget).toEqual('all')
|
||
expect(word!.expiresAt).toEqual(expiresAt)
|
||
})
|
||
|
||
it('single-hash #, no insert', async () => {
|
||
await agent.addMutedWord({
|
||
value: '#',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
|
||
// sanitized to empty string, not inserted
|
||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||
})
|
||
|
||
it('multi-hash ##, inserts #', async () => {
|
||
await agent.addMutedWord({
|
||
value: '##',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((m) => m.value === '#'),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('multi-hash ##hashtag, inserts #hashtag', async () => {
|
||
await agent.addMutedWord({
|
||
value: '##hashtag',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((w) => w.value === '#hashtag'),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('hash emoji #️⃣, inserts #️⃣', async () => {
|
||
await agent.addMutedWord({
|
||
value: '#️⃣',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('hash emoji w/leading hash ##️⃣, inserts #️⃣', async () => {
|
||
await agent.addMutedWord({
|
||
value: '##️⃣',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((m) => m.value === '#️⃣'),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('hash emoji with double leading hash ###️⃣, inserts ##️⃣', async () => {
|
||
await agent.addMutedWord({
|
||
value: '###️⃣',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((m) => m.value === '##️⃣'),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it(`includes apostrophes e.g. Bluesky's`, async () => {
|
||
await agent.addMutedWord({
|
||
value: `Bluesky's`,
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { mutedWords } = (await agent.getPreferences()).moderationPrefs
|
||
|
||
expect(mutedWords.find((m) => m.value === `Bluesky's`)).toBeTruthy()
|
||
})
|
||
|
||
describe(`invalid characters`, () => {
|
||
it('#<zws>, no insert', async () => {
|
||
await agent.addMutedWord({
|
||
value: '#',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||
})
|
||
|
||
it('#<zws>ab, inserts ab', async () => {
|
||
await agent.addMutedWord({
|
||
value: '#ab',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(moderationPrefs.mutedWords.length).toEqual(1)
|
||
})
|
||
|
||
it('phrase with newline, inserts phrase without newline', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'test value\n with newline',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'test value with newline',
|
||
),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('phrase with newlines, inserts phrase without newlines', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'test value\n\r with newline',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'test value with newline',
|
||
),
|
||
).toBeTruthy()
|
||
})
|
||
|
||
it('empty space, no insert', async () => {
|
||
await agent.addMutedWord({
|
||
value: ' ',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||
})
|
||
|
||
it(`' trim ', inserts 'trim'`, async () => {
|
||
await agent.addMutedWord({
|
||
value: ' trim ',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(
|
||
moderationPrefs.mutedWords.find((m) => m.value === 'trim'),
|
||
).toBeTruthy()
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('addMutedWords', () => {
|
||
it('inserts happen sequentially, no clobbering', async () => {
|
||
await agent.addMutedWords([
|
||
{ value: 'a', targets: ['content'], actorTarget: 'all' },
|
||
{ value: 'b', targets: ['content'], actorTarget: 'all' },
|
||
{ value: 'c', targets: ['content'], actorTarget: 'all' },
|
||
])
|
||
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
|
||
expect(moderationPrefs.mutedWords.length).toEqual(3)
|
||
})
|
||
})
|
||
|
||
describe('upsertMutedWords (deprecated)', () => {
|
||
it('no longer upserts, calls addMutedWords', async () => {
|
||
await agent.upsertMutedWords([
|
||
{ value: 'both', targets: ['content'], actorTarget: 'all' },
|
||
])
|
||
await agent.upsertMutedWords([
|
||
{ value: 'both', targets: ['tag'], actorTarget: 'all' },
|
||
])
|
||
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
|
||
expect(moderationPrefs.mutedWords.length).toEqual(2)
|
||
})
|
||
})
|
||
|
||
describe('updateMutedWord', () => {
|
||
it(`word doesn't exist, no update or insert`, async () => {
|
||
await agent.updateMutedWord({
|
||
value: 'word',
|
||
targets: ['tag', 'content'],
|
||
actorTarget: 'all',
|
||
})
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
expect(moderationPrefs.mutedWords.length).toEqual(0)
|
||
})
|
||
|
||
it('updates and sanitizes new value', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'value',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'value',
|
||
)
|
||
|
||
await agent.updateMutedWord({
|
||
...word!,
|
||
value: '#new value',
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
const updatedWord = b.moderationPrefs.mutedWords.find(
|
||
(m) => m.id === word!.id,
|
||
)
|
||
|
||
expect(updatedWord!.value).toEqual('new value')
|
||
expect(updatedWord).toHaveProperty('targets', ['content'])
|
||
})
|
||
|
||
it('updates targets', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'word',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'word',
|
||
)
|
||
|
||
await agent.updateMutedWord({
|
||
...word!,
|
||
targets: ['content'],
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toHaveProperty('targets', ['content'])
|
||
})
|
||
|
||
it('updates actorTarget', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'value',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'value',
|
||
)
|
||
|
||
await agent.updateMutedWord({
|
||
...word!,
|
||
actorTarget: 'exclude-following',
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toHaveProperty('actorTarget', 'exclude-following')
|
||
})
|
||
|
||
it('updates expiresAt', async () => {
|
||
const expiresAt = new Date(Date.now() + 6e3).toISOString()
|
||
const expiresAt2 = new Date(Date.now() + 10e3).toISOString()
|
||
await agent.addMutedWord({
|
||
value: 'value',
|
||
targets: ['content'],
|
||
expiresAt,
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'value',
|
||
)
|
||
|
||
await agent.updateMutedWord({
|
||
...word!,
|
||
expiresAt: expiresAt2,
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toHaveProperty('expiresAt', expiresAt2)
|
||
})
|
||
|
||
it(`doesn't update if value is sanitized to be falsy`, async () => {
|
||
await agent.addMutedWord({
|
||
value: 'rug',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'rug',
|
||
)
|
||
|
||
await agent.updateMutedWord({
|
||
...word!,
|
||
value: '',
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toHaveProperty('value', 'rug')
|
||
})
|
||
})
|
||
|
||
describe('removeMutedWord', () => {
|
||
it('removes word', async () => {
|
||
await agent.addMutedWord({
|
||
value: 'word',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'word',
|
||
)
|
||
|
||
await agent.removeMutedWord(word!)
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toBeFalsy()
|
||
})
|
||
|
||
it(`word doesn't exist, no action`, async () => {
|
||
await agent.addMutedWord({
|
||
value: 'word',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
const a = await agent.getPreferences()
|
||
const word = a.moderationPrefs.mutedWords.find(
|
||
(m) => m.value === 'word',
|
||
)
|
||
|
||
await agent.removeMutedWord({
|
||
value: 'another',
|
||
targets: [],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(
|
||
b.moderationPrefs.mutedWords.find((m) => m.id === word!.id),
|
||
).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
describe('removeMutedWords', () => {
|
||
it(`removes sequentially, no clobbering`, async () => {
|
||
await agent.addMutedWords([
|
||
{ value: 'a', targets: ['content'], actorTarget: 'all ' },
|
||
{ value: 'b', targets: ['content'], actorTarget: 'all ' },
|
||
{ value: 'c', targets: ['content'], actorTarget: 'all ' },
|
||
])
|
||
|
||
const a = await agent.getPreferences()
|
||
await agent.removeMutedWords(a.moderationPrefs.mutedWords)
|
||
const b = await agent.getPreferences()
|
||
|
||
expect(b.moderationPrefs.mutedWords.length).toEqual(0)
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('legacy muted words', () => {
|
||
let agent: AtpAgent
|
||
|
||
async function updatePreferences(
|
||
agent: AtpAgent,
|
||
cb: (
|
||
prefs: AppBskyActorDefs.Preferences,
|
||
) => AppBskyActorDefs.Preferences | false,
|
||
) {
|
||
const res = await agent.app.bsky.actor.getPreferences({})
|
||
const newPrefs = cb(res.data.preferences)
|
||
if (newPrefs === false) {
|
||
return
|
||
}
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: newPrefs,
|
||
})
|
||
}
|
||
|
||
async function addLegacyMutedWord(mutedWord: AppBskyActorDefs.MutedWord) {
|
||
await updatePreferences(agent, (prefs) => {
|
||
let mutedWordsPref = prefs.findLast(
|
||
(pref) =>
|
||
AppBskyActorDefs.isMutedWordsPref(pref) &&
|
||
AppBskyActorDefs.validateMutedWordsPref(pref).success,
|
||
)
|
||
|
||
const newMutedWord: AppBskyActorDefs.MutedWord = {
|
||
value: mutedWord.value,
|
||
targets: mutedWord.targets,
|
||
actorTarget: 'all',
|
||
}
|
||
|
||
if (
|
||
mutedWordsPref &&
|
||
AppBskyActorDefs.isMutedWordsPref(mutedWordsPref)
|
||
) {
|
||
mutedWordsPref.items.push(newMutedWord)
|
||
} else {
|
||
// if the pref doesn't exist, create it
|
||
mutedWordsPref = {
|
||
items: [newMutedWord],
|
||
}
|
||
}
|
||
|
||
return prefs
|
||
.filter((p) => !AppBskyActorDefs.isMutedWordsPref(p))
|
||
.concat([
|
||
{
|
||
...mutedWordsPref,
|
||
$type: 'app.bsky.actor.defs#mutedWordsPref',
|
||
},
|
||
])
|
||
})
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user7-1.test',
|
||
email: 'user7-1@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
afterEach(async () => {
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
await agent.removeMutedWords(moderationPrefs.mutedWords)
|
||
})
|
||
|
||
describe(`upsertMutedWords (and addMutedWord)`, () => {
|
||
it(`adds new word, migrates old words`, async () => {
|
||
await addLegacyMutedWord({
|
||
value: 'word',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
{
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
const word = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word',
|
||
)
|
||
expect(word).toBeTruthy()
|
||
expect(word!.id).toBeFalsy()
|
||
}
|
||
|
||
await agent.upsertMutedWords([
|
||
{ value: 'word2', targets: ['tag'], actorTarget: 'all' },
|
||
])
|
||
|
||
{
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
const word = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word',
|
||
)
|
||
const word2 = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word2',
|
||
)
|
||
|
||
expect(word!.id).toBeTruthy()
|
||
expect(word2!.id).toBeTruthy()
|
||
}
|
||
})
|
||
})
|
||
|
||
describe(`updateMutedWord`, () => {
|
||
it(`updates legacy word, migrates old words`, async () => {
|
||
await addLegacyMutedWord({
|
||
value: 'word',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
await addLegacyMutedWord({
|
||
value: 'word2',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
await agent.updateMutedWord({
|
||
value: 'word',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
{
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
const word = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word',
|
||
)
|
||
const word2 = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word2',
|
||
)
|
||
|
||
expect(moderationPrefs.mutedWords.length).toEqual(2)
|
||
expect(word!.id).toBeTruthy()
|
||
expect(word!.targets).toEqual(['tag'])
|
||
expect(word2!.id).toBeTruthy()
|
||
}
|
||
})
|
||
})
|
||
|
||
describe(`removeMutedWord`, () => {
|
||
it(`removes legacy word, migrates old words`, async () => {
|
||
await addLegacyMutedWord({
|
||
value: 'word',
|
||
targets: ['content'],
|
||
actorTarget: 'all',
|
||
})
|
||
await addLegacyMutedWord({
|
||
value: 'word2',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
await agent.removeMutedWord({
|
||
value: 'word',
|
||
targets: ['tag'],
|
||
actorTarget: 'all',
|
||
})
|
||
|
||
{
|
||
const { moderationPrefs } = await agent.getPreferences()
|
||
const word = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word',
|
||
)
|
||
const word2 = moderationPrefs.mutedWords.find(
|
||
(w) => w.value === 'word2',
|
||
)
|
||
|
||
expect(moderationPrefs.mutedWords.length).toEqual(1)
|
||
expect(word).toBeFalsy()
|
||
expect(word2!.id).toBeTruthy()
|
||
}
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('hidden posts', () => {
|
||
let agent: AtpAgent
|
||
const postUri = 'at://did:plc:fake/app.bsky.feed.post/fake'
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user8.test',
|
||
email: 'user8@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
it('hidePost', async () => {
|
||
await agent.hidePost(postUri)
|
||
await agent.hidePost(postUri) // double, should dedupe
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'moderationPrefs.hiddenPosts',
|
||
[postUri],
|
||
)
|
||
})
|
||
|
||
it('unhidePost', async () => {
|
||
await agent.unhidePost(postUri)
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'moderationPrefs.hiddenPosts',
|
||
[],
|
||
)
|
||
// no issues calling a second time
|
||
await agent.unhidePost(postUri)
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'moderationPrefs.hiddenPosts',
|
||
[],
|
||
)
|
||
})
|
||
})
|
||
|
||
describe(`saved feeds v2`, () => {
|
||
let agent: AtpAgent
|
||
let i = 0
|
||
const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
|
||
const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}`
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user9.test',
|
||
email: 'user9@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
beforeEach(async () => {
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [],
|
||
})
|
||
})
|
||
|
||
describe(`addSavedFeeds`, () => {
|
||
it('works', async () => {
|
||
const feed = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([feed])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
...feed,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
|
||
it('throws if feed is specified and list provided', async () => {
|
||
const list = listUri()
|
||
await expect(() =>
|
||
agent.addSavedFeeds([
|
||
{
|
||
type: 'feed',
|
||
value: list,
|
||
pinned: true,
|
||
},
|
||
]),
|
||
).rejects.toThrow()
|
||
})
|
||
|
||
it('throws if list is specified and feed provided', async () => {
|
||
const feed = feedUri()
|
||
await expect(() =>
|
||
agent.addSavedFeeds([
|
||
{
|
||
type: 'list',
|
||
value: feed,
|
||
pinned: true,
|
||
},
|
||
]),
|
||
).rejects.toThrow()
|
||
})
|
||
|
||
it(`timeline`, async () => {
|
||
const feeds = await agent.addSavedFeeds([
|
||
{
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
])
|
||
const prefs = await agent.getPreferences()
|
||
expect(
|
||
prefs.savedFeeds.filter((f) => f.type === 'timeline'),
|
||
).toStrictEqual(feeds)
|
||
})
|
||
|
||
it(`allows duplicates`, async () => {
|
||
const feed = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([feed])
|
||
await agent.addSavedFeeds([feed])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
...feed,
|
||
id: expect.any(String),
|
||
},
|
||
{
|
||
...feed,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
|
||
it(`adds multiple`, async () => {
|
||
const a = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const b = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([a, b])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
...a,
|
||
id: expect.any(String),
|
||
},
|
||
{
|
||
...b,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
|
||
it(`appends multiple`, async () => {
|
||
const a = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const b = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
const c = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const d = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([a, b])
|
||
await agent.addSavedFeeds([c, d])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
...a,
|
||
id: expect.any(String),
|
||
},
|
||
{
|
||
...c,
|
||
id: expect.any(String),
|
||
},
|
||
{
|
||
...b,
|
||
id: expect.any(String),
|
||
},
|
||
{
|
||
...d,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe(`removeSavedFeeds`, () => {
|
||
it('works', async () => {
|
||
const feed = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const savedFeeds = await agent.addSavedFeeds([feed])
|
||
await agent.removeSavedFeeds([savedFeeds[0].id])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([])
|
||
})
|
||
})
|
||
|
||
describe(`overwriteSavedFeeds`, () => {
|
||
it(`dedupes by id, takes last, preserves order based on last found`, async () => {
|
||
const a = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const b = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
await agent.overwriteSavedFeeds([a, b, a])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([b, a])
|
||
})
|
||
|
||
it(`preserves order`, async () => {
|
||
const a = feedUri()
|
||
const b = feedUri()
|
||
const c = feedUri()
|
||
const d = feedUri()
|
||
|
||
await agent.overwriteSavedFeeds([
|
||
{
|
||
id: TID.nextStr(),
|
||
type: 'timeline',
|
||
value: a,
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: b,
|
||
pinned: false,
|
||
},
|
||
{
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: c,
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: d,
|
||
pinned: false,
|
||
},
|
||
])
|
||
|
||
const { savedFeeds } = await agent.getPreferences()
|
||
expect(savedFeeds.filter((f) => f.pinned)).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: a,
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: c,
|
||
pinned: true,
|
||
},
|
||
])
|
||
expect(savedFeeds.filter((f) => !f.pinned)).toEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: b,
|
||
pinned: false,
|
||
},
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: d,
|
||
pinned: false,
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe(`updateSavedFeeds`, () => {
|
||
it(`updates affect order, saved last, new pins last`, async () => {
|
||
const a = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const b = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
const c = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
|
||
await agent.overwriteSavedFeeds([a, b, c])
|
||
await agent.updateSavedFeeds([
|
||
{
|
||
...b,
|
||
pinned: false,
|
||
},
|
||
])
|
||
|
||
const prefs1 = await agent.getPreferences()
|
||
expect(prefs1.savedFeeds).toStrictEqual([
|
||
a,
|
||
c,
|
||
{
|
||
...b,
|
||
pinned: false,
|
||
},
|
||
])
|
||
|
||
await agent.updateSavedFeeds([
|
||
{
|
||
...b,
|
||
pinned: true,
|
||
},
|
||
])
|
||
|
||
const prefs2 = await agent.getPreferences()
|
||
expect(prefs2.savedFeeds).toStrictEqual([a, c, b])
|
||
})
|
||
|
||
it(`cannot override original id`, async () => {
|
||
const a = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: true,
|
||
}
|
||
await agent.overwriteSavedFeeds([a])
|
||
await agent.updateSavedFeeds([
|
||
{
|
||
...a,
|
||
pinned: false,
|
||
id: TID.nextStr(),
|
||
},
|
||
])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([a])
|
||
})
|
||
|
||
it(`updates multiple`, async () => {
|
||
const a = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
const b = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
const c = {
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
|
||
await agent.overwriteSavedFeeds([a, b, c])
|
||
await agent.updateSavedFeeds([
|
||
{
|
||
...b,
|
||
pinned: true,
|
||
},
|
||
{
|
||
...c,
|
||
pinned: true,
|
||
},
|
||
])
|
||
|
||
const prefs1 = await agent.getPreferences()
|
||
expect(prefs1.savedFeeds).toStrictEqual([
|
||
{
|
||
...b,
|
||
pinned: true,
|
||
},
|
||
{
|
||
...c,
|
||
pinned: true,
|
||
},
|
||
a,
|
||
])
|
||
})
|
||
})
|
||
|
||
describe(`utils`, () => {
|
||
describe(`savedFeedsToUriArrays`, () => {
|
||
const { saved, pinned } = savedFeedsToUriArrays([
|
||
{
|
||
id: '',
|
||
type: 'feed',
|
||
value: 'a',
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: '',
|
||
type: 'feed',
|
||
value: 'b',
|
||
pinned: false,
|
||
},
|
||
{
|
||
id: '',
|
||
type: 'feed',
|
||
value: 'c',
|
||
pinned: true,
|
||
},
|
||
])
|
||
expect(saved).toStrictEqual(['a', 'b', 'c'])
|
||
expect(pinned).toStrictEqual(['a', 'c'])
|
||
})
|
||
|
||
describe(`getSavedFeedType`, () => {
|
||
it(`works`, () => {
|
||
expect(getSavedFeedType('foo')).toBe('unknown')
|
||
expect(getSavedFeedType(feedUri())).toBe('feed')
|
||
expect(getSavedFeedType(listUri())).toBe('list')
|
||
expect(
|
||
getSavedFeedType('at://did:plc:fake/app.bsky.graph.follow/fake'),
|
||
).toBe('unknown')
|
||
})
|
||
})
|
||
|
||
describe(`validateSavedFeed`, () => {
|
||
it(`throws if invalid TID`, () => {
|
||
// really only checks length at time of writing
|
||
expect(() =>
|
||
validateSavedFeed({
|
||
id: 'a',
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}),
|
||
).toThrow()
|
||
})
|
||
|
||
it(`throws if mismatched types`, () => {
|
||
expect(() =>
|
||
validateSavedFeed({
|
||
id: TID.nextStr(),
|
||
type: 'list',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}),
|
||
).toThrow()
|
||
expect(() =>
|
||
validateSavedFeed({
|
||
id: TID.nextStr(),
|
||
type: 'feed',
|
||
value: listUri(),
|
||
pinned: false,
|
||
}),
|
||
).toThrow()
|
||
})
|
||
|
||
it(`ignores values it can't validate`, () => {
|
||
expect(() =>
|
||
validateSavedFeed({
|
||
id: TID.nextStr(),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: false,
|
||
}),
|
||
).not.toThrow()
|
||
expect(() =>
|
||
validateSavedFeed({
|
||
id: TID.nextStr(),
|
||
type: 'unknown',
|
||
value: 'could be @nyt4!ng',
|
||
pinned: false,
|
||
}),
|
||
).not.toThrow()
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
describe(`saved feeds v2: migration scenarios`, () => {
|
||
let agent: AtpAgent
|
||
let i = 0
|
||
const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user10.test',
|
||
email: 'user10@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
beforeEach(async () => {
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [],
|
||
})
|
||
})
|
||
|
||
it('CRUD action before migration, no timeline inserted', async () => {
|
||
const feed = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([feed])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
...feed,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
|
||
it('CRUD action AFTER migration, timeline was inserted', async () => {
|
||
await agent.getPreferences()
|
||
const feed = {
|
||
type: 'feed',
|
||
value: feedUri(),
|
||
pinned: false,
|
||
}
|
||
await agent.addSavedFeeds([feed])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
{
|
||
...feed,
|
||
id: expect.any(String),
|
||
},
|
||
])
|
||
})
|
||
|
||
// fresh account OR an old account with no v1 prefs to migrate from
|
||
it(`brand new user, v1 remains undefined`, async () => {
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
])
|
||
// no v1 prefs to populate from
|
||
expect(prefs.feeds).toStrictEqual({
|
||
saved: undefined,
|
||
pinned: undefined,
|
||
})
|
||
})
|
||
|
||
it(`brand new user, v2 does not write to v1`, async () => {
|
||
const a = feedUri()
|
||
// migration happens
|
||
await agent.getPreferences()
|
||
await agent.addSavedFeeds([
|
||
{
|
||
type: 'feed',
|
||
value: a,
|
||
pinned: false,
|
||
},
|
||
])
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: a,
|
||
pinned: false,
|
||
},
|
||
])
|
||
// no v1 prefs to populate from
|
||
expect(prefs.feeds).toStrictEqual({
|
||
saved: undefined,
|
||
pinned: undefined,
|
||
})
|
||
})
|
||
|
||
it(`existing user with v1 prefs, migrates`, async () => {
|
||
const one = feedUri()
|
||
const two = feedUri()
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [one],
|
||
saved: [one, two],
|
||
},
|
||
],
|
||
})
|
||
const prefs = await agent.getPreferences()
|
||
|
||
// deprecated interface receives what it normally would
|
||
expect(prefs.feeds).toStrictEqual({
|
||
pinned: [one],
|
||
saved: [one, two],
|
||
})
|
||
// new interface gets new timeline + old pinned feed
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: one,
|
||
pinned: true,
|
||
},
|
||
{
|
||
id: expect.any(String),
|
||
type: 'feed',
|
||
value: two,
|
||
pinned: false,
|
||
},
|
||
])
|
||
})
|
||
|
||
it('squashes duplicates during migration', async () => {
|
||
const one = feedUri()
|
||
const two = feedUri()
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [one, two],
|
||
saved: [one, two],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
],
|
||
})
|
||
|
||
// performs migration
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.feeds).toStrictEqual({
|
||
pinned: [],
|
||
saved: [],
|
||
})
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
])
|
||
|
||
const res = await agent.app.bsky.actor.getPreferences()
|
||
expect(res.data.preferences).toStrictEqual([
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPrefV2',
|
||
items: [
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [],
|
||
saved: [],
|
||
},
|
||
])
|
||
})
|
||
|
||
it('v2 writes persist to v1, not the inverse', async () => {
|
||
const a = feedUri()
|
||
const b = feedUri()
|
||
const c = feedUri()
|
||
const d = feedUri()
|
||
const e = feedUri()
|
||
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [a, b],
|
||
saved: [a, b],
|
||
},
|
||
],
|
||
})
|
||
|
||
// client updates, migrates to v2
|
||
// a and b are both pinned
|
||
await agent.getPreferences()
|
||
|
||
// new write to v2, c is saved
|
||
await agent.addSavedFeeds([
|
||
{
|
||
type: 'feed',
|
||
value: c,
|
||
pinned: false,
|
||
},
|
||
])
|
||
|
||
// v2 write wrote to v1 also
|
||
const res1 = await agent.app.bsky.actor.getPreferences()
|
||
const v1Pref = res1.data.preferences.find((p) =>
|
||
AppBskyActorDefs.isSavedFeedsPref(p),
|
||
)
|
||
expect(v1Pref).toStrictEqual({
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [a, b],
|
||
saved: [a, b, c],
|
||
})
|
||
|
||
// v1 write occurs, d is added but not to v2
|
||
await agent.addSavedFeed(d)
|
||
|
||
const res3 = await agent.app.bsky.actor.getPreferences()
|
||
const v1Pref3 = res3.data.preferences.find((p) =>
|
||
AppBskyActorDefs.isSavedFeedsPref(p),
|
||
)
|
||
expect(v1Pref3).toStrictEqual({
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [a, b],
|
||
saved: [a, b, c, d],
|
||
})
|
||
|
||
// another new write to v2, pins e
|
||
await agent.addSavedFeeds([
|
||
{
|
||
type: 'feed',
|
||
value: e,
|
||
pinned: true,
|
||
},
|
||
])
|
||
|
||
const res4 = await agent.app.bsky.actor.getPreferences()
|
||
const v1Pref4 = res4.data.preferences.find((p) =>
|
||
AppBskyActorDefs.isSavedFeedsPref(p),
|
||
)
|
||
// v1 pref got v2 write
|
||
expect(v1Pref4).toStrictEqual({
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: [a, b, e],
|
||
saved: [a, b, c, d, e],
|
||
})
|
||
|
||
const final = await agent.getPreferences()
|
||
// d not here bc it was written with v1
|
||
expect(final.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
{ id: expect.any(String), type: 'feed', value: a, pinned: true },
|
||
{ id: expect.any(String), type: 'feed', value: b, pinned: true },
|
||
{ id: expect.any(String), type: 'feed', value: e, pinned: true },
|
||
{ id: expect.any(String), type: 'feed', value: c, pinned: false },
|
||
])
|
||
})
|
||
|
||
it(`filters out invalid values in v1 prefs`, async () => {
|
||
// v1 prefs must be valid AtUris, but they could be any type in theory
|
||
await agent.app.bsky.actor.putPreferences({
|
||
preferences: [
|
||
{
|
||
$type: 'app.bsky.actor.defs#savedFeedsPref',
|
||
pinned: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
|
||
saved: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
|
||
},
|
||
],
|
||
})
|
||
const prefs = await agent.getPreferences()
|
||
expect(prefs.savedFeeds).toStrictEqual([
|
||
{
|
||
id: expect.any(String),
|
||
type: 'timeline',
|
||
value: 'following',
|
||
pinned: true,
|
||
},
|
||
])
|
||
})
|
||
})
|
||
|
||
describe('queued nudges', () => {
|
||
it('queueNudges & dismissNudges', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
await agent.createAccount({
|
||
handle: 'user11.test',
|
||
email: 'user11@test.com',
|
||
password: 'password',
|
||
})
|
||
await agent.bskyAppQueueNudges('first')
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.queuedNudges',
|
||
['first'],
|
||
)
|
||
await agent.bskyAppQueueNudges(['second', 'third'])
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.queuedNudges',
|
||
['first', 'second', 'third'],
|
||
)
|
||
await agent.bskyAppDismissNudges('second')
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.queuedNudges',
|
||
['first', 'third'],
|
||
)
|
||
await agent.bskyAppDismissNudges(['first', 'third'])
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.queuedNudges',
|
||
[],
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('guided tours', () => {
|
||
it('setActiveProgressGuide', async () => {
|
||
const agent = new AtpAgent({ service: network.pds.url })
|
||
|
||
await agent.createAccount({
|
||
handle: 'user12.test',
|
||
email: 'user12@test.com',
|
||
password: 'password',
|
||
})
|
||
|
||
await agent.bskyAppSetActiveProgressGuide({
|
||
guide: 'test-guide',
|
||
numThings: 0,
|
||
})
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.activeProgressGuide.guide',
|
||
'test-guide',
|
||
)
|
||
await agent.bskyAppSetActiveProgressGuide({
|
||
guide: 'test-guide',
|
||
numThings: 1,
|
||
})
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.activeProgressGuide.guide',
|
||
'test-guide',
|
||
)
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.activeProgressGuide.numThings',
|
||
1,
|
||
)
|
||
await agent.bskyAppSetActiveProgressGuide(undefined)
|
||
await expect(agent.getPreferences()).resolves.toHaveProperty(
|
||
'bskyAppState.activeProgressGuide',
|
||
undefined,
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('nuxs', () => {
|
||
let agent: AtpAgent
|
||
|
||
const nux = {
|
||
id: 'a',
|
||
completed: false,
|
||
data: '{}',
|
||
expiresAt: new Date(Date.now() + 6e3).toISOString(),
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
agent = new AtpAgent({ service: network.pds.url })
|
||
|
||
await agent.createAccount({
|
||
handle: 'nuxs.test',
|
||
email: 'nuxs@test.com',
|
||
password: 'password',
|
||
})
|
||
})
|
||
|
||
it('bskyAppUpsertNux', async () => {
|
||
// never duplicates
|
||
await agent.bskyAppUpsertNux(nux)
|
||
await agent.bskyAppUpsertNux(nux)
|
||
await agent.bskyAppUpsertNux(nux)
|
||
|
||
const prefs = await agent.getPreferences()
|
||
const nuxs = prefs.bskyAppState.nuxs
|
||
|
||
expect(nuxs.length).toEqual(1)
|
||
expect(nuxs.find((n) => n.id === nux.id)).toEqual(nux)
|
||
})
|
||
|
||
it('bskyAppUpsertNux completed', async () => {
|
||
// never duplicates
|
||
await agent.bskyAppUpsertNux({
|
||
...nux,
|
||
completed: true,
|
||
})
|
||
|
||
const prefs = await agent.getPreferences()
|
||
const nuxs = prefs.bskyAppState.nuxs
|
||
|
||
expect(nuxs.length).toEqual(1)
|
||
expect(nuxs.find((n) => n.id === nux.id)?.completed).toEqual(true)
|
||
})
|
||
|
||
it('bskyAppRemoveNuxs', async () => {
|
||
await agent.bskyAppRemoveNuxs([nux.id])
|
||
|
||
const prefs = await agent.getPreferences()
|
||
const nuxs = prefs.bskyAppState.nuxs
|
||
|
||
expect(nuxs.length).toEqual(0)
|
||
})
|
||
|
||
it('bskyAppUpsertNux validates nux', async () => {
|
||
// @ts-expect-error
|
||
expect(() => agent.bskyAppUpsertNux({ name: 'a' })).rejects.toThrow()
|
||
expect(() =>
|
||
agent.bskyAppUpsertNux({ id: 'a', completed: false, foo: 'bar' }),
|
||
).rejects.toThrow()
|
||
})
|
||
})
|
||
|
||
// end
|
||
})
|
||
})
|
||
|
||
const byType = (a, b) => a.$type.localeCompare(b.$type)
|