@@ -3,7 +3,7 @@ import { CssVar } from '../types/Theme'
33import { useEffect , useState } from 'react'
44import { useTranslation } from 'react-i18next'
55import { useClient } from '../contexts/Client'
6- import { NotFoundError } from '@concrnt/client'
6+ import { NotFoundError , type Document , type PolicyEntry } from '@concrnt/client'
77import { useNavigate } from 'react-router-dom'
88import { Schemas , semantics , type Timeline } from '@concrnt/worldlib'
99import { MdPlaylistAdd } from 'react-icons/md'
@@ -47,11 +47,17 @@ export const Activitypub = () => {
4747
4848 const homeTimelineRegex = new RegExp ( `^cckv://${ client . ccid } /concrnt\\.world/profiles/([^/]+)/home-timeline$` )
4949 const inboxUri = apInboxKey ( client . ccid )
50+ const allowWriters = 'https://policy.concrnt.world/t/allow-writers.json'
5051
5152 // ActivityPubタイムラインはリスト未登録だとknownCommunitiesに出ない上、
5253 // どのリストにも無いと受信した投稿を見る手段が無いので警告を出す
5354 const [ pinnedLists ] = useSubscribe ( client . pinnedLists )
5455 const [ inboxListed , setInboxListed ] = useState ( true )
56+
57+ // inboxのpolicyがブリッジの現serviceAccountIdを許可していないと配送が全て拒否される
58+ // (誤ったIDが広告された期間に設定画面を開くと壊れたpolicyが書き込まれる事故があった)
59+ const [ inboxPolicyBroken , setInboxPolicyBroken ] = useState ( false )
60+ const [ repairError , setRepairError ] = useState ( false )
5561 useEffect ( ( ) => {
5662 Promise . all (
5763 pinnedLists . map ( async ( pin ) => {
@@ -98,7 +104,7 @@ export const Activitypub = () => {
98104 const defaultPolicy = {
99105 entries : [
100106 {
101- url : 'https://policy.concrnt.world/t/allow-writers.json' ,
107+ url : allowWriters ,
102108 params : {
103109 entities : [ res . serviceAccountId ]
104110 }
@@ -109,19 +115,40 @@ export const Activitypub = () => {
109115 client . api
110116 . getDocument < any > ( inboxUri )
111117 . then ( ( doc ) => {
118+ // 配送時のrequesterはブリッジのserviceAccountIdなので、allow-writersの
119+ // entitiesに現在のIDが含まれていないと受信が全て拒否される
120+ const broken = ! (
121+ doc . policy ?. entries ?. some (
122+ ( e : PolicyEntry ) =>
123+ e . url === allowWriters &&
124+ Array . isArray ( e . params ?. entities ) &&
125+ e . params . entities . includes ( res . serviceAccountId )
126+ ) ?? false
127+ )
112128 // 旧クライアントが作ったinboxはuserTimelineスキーマで名前を持てないため、
113129 // communityTimelineスキーマに上書き修復する(ポリシーは維持)
114- if ( doc . schema === Schemas . communityTimeline && doc . value ?. name ) return
130+ if ( doc . schema === Schemas . communityTimeline && doc . value ?. name ) {
131+ setInboxPolicyBroken ( broken )
132+ return
133+ }
115134 console . log ( 'Inbox has no metadata. repairing...' )
116- client . api . commit ( {
117- kind : 'record' as const ,
118- key : inboxUri ,
119- author : client . ccid ,
120- schema : Schemas . communityTimeline ,
121- value : inboxValue ,
122- createdAt : new Date ( ) ,
123- policy : doc . policy ?? defaultPolicy
124- } )
135+ client . api
136+ . commit ( {
137+ kind : 'record' as const ,
138+ key : inboxUri ,
139+ author : client . ccid ,
140+ schema : Schemas . communityTimeline ,
141+ value : inboxValue ,
142+ createdAt : new Date ( ) ,
143+ policy : doc . policy ?? defaultPolicy
144+ } )
145+ // 警告(=修復ボタン)はこのcommitの完了後に出す。in-flightの自動commitが
146+ // 修復ボタンのcommit後に着弾して誤policyを書き戻す競合を避けるため
147+ . then ( ( ) => setInboxPolicyBroken ( doc . policy ? broken : false ) )
148+ . catch ( ( err ) => {
149+ console . log ( err )
150+ setInboxPolicyBroken ( broken )
151+ } )
125152 } )
126153 . catch ( ( err ) => {
127154 if ( err instanceof NotFoundError ) {
@@ -143,6 +170,57 @@ export const Activitypub = () => {
143170 } )
144171 } , [ ] )
145172
173+ const repairInboxPolicy = async ( ) : Promise < void > => {
174+ setRepairError ( false )
175+ try {
176+ // マウント時のIDは誤ったIDが広告されていた期間のものかもしれないので、押下時点で取り直す
177+ const info = await client . api . callConcrntApi < ApServerInfo > (
178+ client . server . domain ,
179+ 'net.concrnt.activitypub.info' ,
180+ { }
181+ )
182+ // staleキャッシュ由来の誤検知のまま上書きしないよう、書き込み前にno-cacheで確定させる
183+ let doc : Document < any > | null = null
184+ try {
185+ doc = await client . api . getDocument < any > ( inboxUri , undefined , { cache : 'no-cache' } )
186+ } catch ( err ) {
187+ if ( ! ( err instanceof NotFoundError ) ) throw err
188+ }
189+ const alreadyOk =
190+ doc ?. policy ?. entries ?. some (
191+ ( e : PolicyEntry ) =>
192+ e . url === allowWriters &&
193+ Array . isArray ( e . params ?. entities ) &&
194+ e . params . entities . includes ( info . serviceAccountId )
195+ ) ?? false
196+ if ( alreadyOk ) {
197+ setInboxPolicyBroken ( false )
198+ return
199+ }
200+ // allow-writers系entryは全て除去して正規形1本に置換(誤IDに書き込み権を残さない)。
201+ // restrict-readers等の他entryは温存
202+ const entries = ( doc ?. policy ?. entries ?? [ ] ) . filter ( ( e : PolicyEntry ) => e . url !== allowWriters )
203+ entries . push ( { url : allowWriters , params : { entities : [ info . serviceAccountId ] } } )
204+ const value =
205+ doc ?. schema === Schemas . communityTimeline && doc . value ?. name
206+ ? doc . value
207+ : { name : 'ActivityPub' , shortname : 'activitypub' , description : 'ActivityPub home stream' }
208+ await client . api . commit ( {
209+ kind : 'record' as const ,
210+ key : inboxUri ,
211+ author : client . ccid ,
212+ schema : Schemas . communityTimeline ,
213+ value,
214+ createdAt : new Date ( ) ,
215+ policy : { entries }
216+ } )
217+ setInboxPolicyBroken ( false )
218+ } catch ( err ) {
219+ console . error ( 'failed to repair inbox policy:' , err )
220+ setRepairError ( true )
221+ }
222+ }
223+
146224 const updateListenTimelines = ( ) => {
147225 const listenTimelines = [
148226 ...( listenHome ? [ semantics . homeTimeline ( client . ccid , listenProfile ) ] : [ ] ) ,
@@ -235,6 +313,21 @@ export const Activitypub = () => {
235313 </ Button >
236314 </ div >
237315 ) }
316+ { inboxPolicyBroken && (
317+ < div style = { { display : 'flex' , alignItems : 'center' , gap : CssVar . space ( 1 ) } } >
318+ < Text variant = "caption" style = { { color : 'red' } } >
319+ { t ( 'inboxPolicyBroken' ) }
320+ </ Text >
321+ < Button variant = "text" busyChildren = { t ( 'repairing' ) } onClick = { repairInboxPolicy } >
322+ { t ( 'repair' ) }
323+ </ Button >
324+ </ div >
325+ ) }
326+ { repairError && (
327+ < Text variant = "caption" style = { { color : 'red' } } >
328+ { t ( 'repairFailed' ) }
329+ </ Text >
330+ ) }
238331 < Divider />
239332 < Text > { t ( 'forwardTimeline' ) } </ Text >
240333 < Text > { t ( 'forwardTimelineDesc' ) } </ Text >
0 commit comments