Skip to content

Commit

Permalink
✨ Takedown policy association (#261)
Browse files Browse the repository at this point in the history
* ✨ Allow associating policies with takedowns

* ✨ Add policy manager area

* ✨ Add policy filter to events tab

* ⬆️ Update @atproto/ozone and @atproto/api version

* ✨ Add takedown policy association with bulk takedown

* ✨ Replace react hook based pathname with browser api
  • Loading branch information
foysalit authored Jan 3, 2025
1 parent bda724c commit ae2fdb0
Show file tree
Hide file tree
Showing 24 changed files with 746 additions and 63 deletions.
48 changes: 38 additions & 10 deletions app/actions/ModActionPanel/QuickAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
import { SubjectTag } from 'components/tags/SubjectTag'
import { HighProfileWarning } from '@/repositories/HighProfileWarning'
import { EmailComposer } from 'components/email/Composer'
import { ActionPolicySelector } from '@/reports/ModerationForm/ActionPolicySelector'

const FORM_ID = 'mod-action-panel'
const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 })
Expand Down Expand Up @@ -266,6 +267,10 @@ function Form(
coreEvent.durationInHours = Number(formData.get('durationInHours'))
}

if (isTakedownEvent && formData.get('policies')) {
coreEvent.policies = [String(formData.get('policies'))]
}

if (
(isTakedownEvent || isAckEvent) &&
formData.get('acknowledgeAccountSubjects')
Expand Down Expand Up @@ -333,6 +338,10 @@ function Form(
throw new Error('blob-selection-required')
}

if (isTakedownEvent && !coreEvent.policies) {
throw new Error('policy-selection-required')
}

// This block handles an edge case where a label may be applied to profile record and then the profile record is updated by the user.
// In that state, if the moderator reverts the label, the event is emitted for the latest CID of the profile entry which does NOT revert
// the label applied to the old CID.
Expand Down Expand Up @@ -666,16 +675,35 @@ function Form(
<ModEventDetailsPopover modEventType={modEventType} />
</div>
{shouldShowDurationInHoursField && (
<FormLabel
label=""
htmlFor="durationInHours"
className={`mb-3 mt-2`}
>
<ActionDurationSelector
action={modEventType}
labelText={isMuteEvent ? 'Mute duration' : ''}
/>
</FormLabel>
<div className="flex flex-row gap-2">
<FormLabel
label=""
htmlFor="durationInHours"
className={`mb-3 mt-2`}
>
<ActionDurationSelector
action={modEventType}
onChange={(e) => {
if (e.target.value === '0') {
// When permanent takedown is selected, auto check ack all checkbox
const ackAllCheckbox =
document.querySelector<HTMLInputElement>(
'input[name="acknowledgeAccountSubjects"]',
)
if (ackAllCheckbox && !ackAllCheckbox.checked) {
ackAllCheckbox.checked = true
}
}
}}
labelText={isMuteEvent ? 'Mute duration' : ''}
/>
</FormLabel>
{isTakedownEvent && (
<div className="mt-2 w-full">
<ActionPolicySelector name="policies" />
</div>
)}
</div>
)}

{isMuteReporterEvent && (
Expand Down
9 changes: 8 additions & 1 deletion app/configure/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ import { WorkspacePanel } from '@/workspace/Panel'
import { useWorkspaceOpener } from '@/common/useWorkspaceOpener'
import { SetsConfig } from '@/config/Sets'
import { ProtectedTagsConfig } from '@/config/ProtectedTags'
import { PoliciesConfig } from '@/config/Policies'

enum Views {
Configure,
Members,
Sets,
ProtectedTags,
Policies,
}

const TabKeys = {
configure: Views.Configure,
members: Views.Members,
sets: Views.Sets,
protectedTags: Views.ProtectedTags,
policies: Views.Policies,
}

export default function ConfigurePageContent() {
Expand Down Expand Up @@ -73,6 +76,10 @@ export default function ConfigurePageContent() {
view: Views.Sets,
label: 'Sets',
},
{
view: Views.Policies,
label: 'Policies',
},
{
view: Views.ProtectedTags,
label: 'Protected Tags',
Expand All @@ -90,8 +97,8 @@ export default function ConfigurePageContent() {
{currentView === Views.Configure && <LabelerConfig />}
{currentView === Views.Members && <MemberConfig />}
{currentView === Views.Sets && <SetsConfig />}
{currentView === Views.Sets && <SetsConfig />}
{currentView === Views.ProtectedTags && <ProtectedTagsConfig />}
{currentView === Views.Policies && <PoliciesConfig />}

<ModActionPanelQuick
open={!!quickOpenParam}
Expand Down
101 changes: 101 additions & 0 deletions components/config/Policies.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ActionButton, LinkButton } from '@/common/buttons'
import { Input } from '@/common/forms'
import { PolicyEditor } from '@/setting/policy/Editor'
import { PolicyList } from '@/setting/policy/List'
import { usePolicyListSetting } from '@/setting/policy/usePolicyList'
import { createPolicyPageLink } from '@/setting/policy/utils'
import { useServerConfig } from '@/shell/ConfigurationContext'
import { ToolsOzoneTeamDefs } from '@atproto/api'
import { PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { useRouter, useSearchParams } from 'next/navigation'

export function PoliciesConfig() {
const router = useRouter()
const searchParams = useSearchParams()
const searchQuery = searchParams.get('search')
const { role } = useServerConfig()
const canManagePolicies = role === ToolsOzoneTeamDefs.ROLEADMIN
const showPoliciesCreateForm = searchParams.has('create')

return (
<div className="pt-4">
<div className="flex flex-row justify-between mb-4">
{typeof searchQuery === 'string' ? (
<>
<Input
type="text"
autoFocus
className="w-3/4"
placeholder="Search policies..."
value={searchQuery}
onChange={(e) => {
const url = createPolicyPageLink({ search: e.target.value })
router.push(url)
}}
/>{' '}
<LinkButton
size="sm"
className="ml-1"
appearance="outlined"
href={createPolicyPageLink({})}
>
Cancel
</LinkButton>
</>
) : (
<>
<div className="flex flex-row items-center">
<h4 className="font-medium text-gray-700 dark:text-gray-100">
Manage Policies
</h4>
</div>
{!showPoliciesCreateForm && (
<div className="flex flex-row items-center">
{canManagePolicies && (
<LinkButton
size="sm"
appearance="primary"
href={createPolicyPageLink({ create: 'true' })}
>
<PlusIcon className="h-3 w-3 mr-1" />
<span className="text-xs">Add New Policy</span>
</LinkButton>
)}

<LinkButton
size="sm"
className="ml-1"
appearance="outlined"
href={createPolicyPageLink({ search: '' })}
>
<MagnifyingGlassIcon className="h-4 w-4" />
</LinkButton>
</div>
)}
</>
)}
</div>
{showPoliciesCreateForm && (
<div className="mb-4">
<PolicyEditor
onCancel={() => {
const url = createPolicyPageLink({})
router.push(url)
}}
onSuccess={() => {
const url = createPolicyPageLink({})
router.push(url)
}}
/>
</div>
)}

<PolicyList
{...{
searchQuery,
canEdit: canManagePolicies,
}}
/>
</div>
)
}
31 changes: 30 additions & 1 deletion components/mod-event/EventItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { useConfigurationContext } from '@/shell/ConfigurationContext'
import { ItemTitle } from './ItemTitle'
import { PreviewCard } from '@/common/PreviewCard'
import { ModEventViewWithDetails } from './useModEventList'
import { ClockIcon, DocumentTextIcon } from '@heroicons/react/24/solid'
import Link from 'next/link'
import { pluralize } from '@/lib/util'

const LinkToAuthor = ({
creatorHandle,
Expand Down Expand Up @@ -219,8 +222,34 @@ const TakedownOrMute = ({
</div>
</div>
{expiresAt && (
<p className="mt-1">Until {dateFormatter.format(expiresAt)}</p>
<p className="mt-1 flex flex-row items-center">
<ClockIcon className="h-3 w-3 inline-block mr-1" />
Until {dateFormatter.format(expiresAt)}
</p>
)}
{ToolsOzoneModerationDefs.isModEventTakedown(modEvent.event) &&
modEvent.event.policies?.length ? (
<p className="pb-1 flex flex-row items-center">
<DocumentTextIcon className="h-3 w-3 inline-block mr-1" />
<i>
Under{' '}
{modEvent.event.policies.map((policy) => {
return (
<Link
key={policy}
prefetch={false}
href={`/configure?tab=policies&search=${policy}`}
>
<u>{`${policy}`}</u>{' '}
</Link>
)
})}
{pluralize(modEvent.event.policies.length, 'policy', {
plural: 'policies',
})}
</i>
</p>
) : null}
{modEvent.event.comment ? (
<p className="pb-1">{`${modEvent.event.comment}`}</p>
) : null}
Expand Down
2 changes: 2 additions & 0 deletions components/mod-event/EventList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const ModEventList = (
isInitialLoadingModEvents,
hasFilter,
commentFilter,
policies,
toggleCommentFilter,
setCommentFilterKeyword,
createdBy,
Expand Down Expand Up @@ -285,6 +286,7 @@ export const ModEventList = (
removedTags,
applyFilterMacro,
changeListFilter,
policies,
}}
/>
)}
Expand Down
15 changes: 15 additions & 0 deletions components/mod-event/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { useState } from 'react'
import { RepoFinder } from '@/repositories/Finder'
import { Dropdown } from '@/common/Dropdown'
import { ChevronDownIcon } from '@heroicons/react/24/solid'
import {
ActionPoliciesSelector,
ActionPolicySelector,
} from '@/reports/ModerationForm/ActionPolicySelector'

export const EventFilterPanel = ({
limit,
Expand Down Expand Up @@ -271,6 +275,17 @@ export const EventFilterPanel = ({
</FormLabel>
</div>
</div>
{types.includes(MOD_EVENTS.TAKEDOWN) && (
<div className="flex flex-row gap-2 mt-2">
<FormLabel label="Policy" className="flex-1">
<ActionPoliciesSelector
onSelect={(policies) => {
changeListFilter({ field: 'policies', value: policies })
}}
/>
</FormLabel>
</div>
)}
{types.includes(MOD_EVENTS.TAG) && (
<div className="flex flex-row gap-2 mt-2">
<FormLabel label="Added Tags" className="flex-1 max-w-sm">
Expand Down
8 changes: 8 additions & 0 deletions components/mod-event/useModEventList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const initialListState = {
removedLabels: [],
addedTags: '',
removedTags: '',
policies: [],
showContentPreview: false,
limit: 25,
}
Expand Down Expand Up @@ -157,6 +158,7 @@ type EventListFilterPayload =
| { field: 'removedLabels'; value: string[] }
| { field: 'addedTags'; value: string }
| { field: 'removedTags'; value: string }
| { field: 'policies'; value: string[] }
| { field: 'limit'; value: number }

type EventListAction =
Expand Down Expand Up @@ -246,6 +248,7 @@ export const useModEventList = (
addedTags,
removedTags,
reportTypes,
policies,
limit,
} = listState
const queryParams: ToolsOzoneModerationQueryEvents.QueryParams = {
Expand Down Expand Up @@ -335,6 +338,10 @@ export const useModEventList = (
})
}

if (filterTypes.includes(MOD_EVENTS.TAKEDOWN) && policies) {
queryParams.policies = policies
}

const { data } = await labelerAgent.tools.ozone.moderation.queryEvents({
...queryParams,
})
Expand Down Expand Up @@ -372,6 +379,7 @@ export const useModEventList = (
listState.createdBy ||
listState.subject ||
listState.oldestFirst ||
listState.policies.length > 0 ||
listState.reportTypes.length > 0 ||
listState.addedLabels.length > 0 ||
listState.removedLabels.length > 0 ||
Expand Down
6 changes: 5 additions & 1 deletion components/reports/ModerationForm/ActionError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const ActionErrors = {
title: 'No blobs selected',
description: 'You must select at least one blob to be diverted',
},
'policy-selection-required': {
title: 'No takedown policy selected',
description: 'You must select the policy used for the takedown',
},
}

export type ActionErrorKey = keyof typeof ActionErrors | string
Expand All @@ -13,7 +17,7 @@ export const ActionError = ({ error }: { error: ActionErrorKey }) => {
if (ActionErrors[error]) {
return (
<Card variation="error">
<h4 className='font-bold'>{ActionErrors[error].title}</h4>
<h4 className="font-bold">{ActionErrors[error].title}</h4>
<p>{ActionErrors[error].description}</p>
</Card>
)
Expand Down
Loading

0 comments on commit ae2fdb0

Please sign in to comment.