Skip to content

Commit

Permalink
Include takendown posts for admins (feature branch) (bluesky-social#1361
Browse files Browse the repository at this point in the history
)

* 🚧 WIP including takendown posts on author feed

* ✨ Add takedown id on posts when including taken down posts

* 🧹 Cleanup the auth verifier and other bsky package code

* ✅ Add test for admin getAuthorFeed

* 🧹 Cleanup lexicon and exclude takedownId

* more explicit plumbing for post hydration w/o requester or with takedown info

* pass along flag for soft-deleted actors

* cleanup getAuthorFeed w/ auth

* reorg getAuthorFeed logic around role/access-based auth

---------

Co-authored-by: Foysal Ahamed <cfaion341@gmail.com>
  • Loading branch information
devinivy and foysalit authored Jul 21, 2023
1 parent 5cc3d81 commit 1f69fcb
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 151 deletions.
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getBlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getBlob({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, res, auth }) => {
const { ref } = ctx.db.db.dynamic
const found = await ctx.db.db
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getBlocks({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getCheckout({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getCommitPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getCommitPath({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getHead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getHead({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getRecord({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did, collection, rkey } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/getRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.getRepo({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
2 changes: 1 addition & 1 deletion packages/pds/src/api/com/atproto/sync/listBlobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.sync.listBlobs({
auth: ctx.optionalAccessOrAdminVerifier,
auth: ctx.optionalAccessOrRoleVerifier,
handler: async ({ params, auth }) => {
const { did } = params
// takedown check for anyone other than an admin or the user
Expand Down
115 changes: 70 additions & 45 deletions packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../../lexicon'
import { FeedKeyset } from '../util/feed'
import { paginate } from '../../../../../db/pagination'
import AppContext from '../../../../../context'
import { FeedRow } from '../../../../services/feed'
import { InvalidRequestError } from '@atproto/xrpc-server'

export default function (server: Server, ctx: AppContext) {
server.app.bsky.feed.getAuthorFeed({
auth: ctx.accessVerifier,
auth: ctx.accessOrRoleVerifier,
handler: async ({ req, params, auth }) => {
const requester = auth.credentials.did
const requester =
auth.credentials.type === 'access' ? auth.credentials.did : null
if (ctx.canProxyRead(req)) {
const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed(
params,
await ctx.serviceAuthHeaders(requester),
requester
? await ctx.serviceAuthHeaders(requester)
: {
// @TODO use authPassthru() once it lands
headers: req.headers.authorization
? { authorization: req.headers.authorization }
: {},
},
)
return {
encoding: 'application/json',
Expand All @@ -22,52 +30,32 @@ export default function (server: Server, ctx: AppContext) {
}

const { actor, limit, cursor } = params
const db = ctx.db.db
const { ref } = db.dynamic

// first verify there is not a block between requester & subject
const blocks = await ctx.services.appView
.graph(ctx.db)
.getBlocks(requester, actor)
if (blocks.blocking) {
throw new InvalidRequestError(
`Requester has blocked actor: ${actor}`,
'BlockedActor',
)
} else if (blocks.blockedBy) {
throw new InvalidRequestError(
`Requester is blocked by actor: $${actor}`,
'BlockedByActor',
)
}

const { ref } = ctx.db.db.dynamic
const accountService = ctx.services.account(ctx.db)
const feedService = ctx.services.appView.feed(ctx.db)
const graphService = ctx.services.appView.graph(ctx.db)

const userLookupCol = actor.startsWith('did:')
? 'did_handle.did'
: 'did_handle.handle'
const actorDidQb = db
.selectFrom('did_handle')
.select('did')
.where(userLookupCol, '=', actor)
.limit(1)
let feedItemsQb = getFeedItemsQb(ctx, { actor })

let feedItemsQb = feedService
.selectFeedItemQb()
.where('originatorDid', '=', actorDidQb)
.where((qb) =>
// Hide reposts of muted content
qb
.where('type', '=', 'post')
.orWhere((qb) =>
accountService.whereNotMuted(qb, requester, [
ref('post.creator'),
]),
),
)
.whereNotExists(graphService.blockQb(requester, [ref('post.creator')]))
// for access-based auth, enforce blocks and mutes
if (requester) {
await assertNoBlocks(ctx, { requester, actor })
feedItemsQb = feedItemsQb
.where((qb) =>
// hide reposts of muted content
qb
.where('type', '=', 'post')
.orWhere((qb) =>
accountService.whereNotMuted(qb, requester, [
ref('post.creator'),
]),
),
)
.whereNotExists(
graphService.blockQb(requester, [ref('post.creator')]),
)
}

const keyset = new FeedKeyset(
ref('feed_item.sortAt'),
Expand All @@ -81,7 +69,9 @@ export default function (server: Server, ctx: AppContext) {
})

const feedItems: FeedRow[] = await feedItemsQb.execute()
const feed = await feedService.hydrateFeed(feedItems, requester)
const feed = await feedService.hydrateFeed(feedItems, requester, {
includeSoftDeleted: auth.credentials.type === 'role', // show takendown content to mods
})

return {
encoding: 'application/json',
Expand All @@ -93,3 +83,38 @@ export default function (server: Server, ctx: AppContext) {
},
})
}

function getFeedItemsQb(ctx: AppContext, opts: { actor: string }) {
const { actor } = opts
const feedService = ctx.services.appView.feed(ctx.db)
const userLookupCol = actor.startsWith('did:')
? 'did_handle.did'
: 'did_handle.handle'
const actorDidQb = ctx.db.db
.selectFrom('did_handle')
.select('did')
.where(userLookupCol, '=', actor)
.limit(1)
return feedService.selectFeedItemQb().where('originatorDid', '=', actorDidQb)
}

// throws when there's a block between the two users
async function assertNoBlocks(
ctx: AppContext,
opts: { requester: string; actor: string },
) {
const { requester, actor } = opts
const graphService = ctx.services.appView.graph(ctx.db)
const blocks = await graphService.getBlocks(requester, actor)
if (blocks.blocking) {
throw new InvalidRequestError(
`Requester has blocked actor: ${actor}`,
'BlockedActor',
)
} else if (blocks.blockedBy) {
throw new InvalidRequestError(
`Requester is blocked by actor: $${actor}`,
'BlockedByActor',
)
}
}
Loading

0 comments on commit 1f69fcb

Please sign in to comment.