Skip to content

Commit ae725b0

Browse files
committed
add ap timeline fixer
1 parent 9549732 commit ae725b0

8 files changed

Lines changed: 223 additions & 27 deletions

File tree

app/src/views/Activitypub.tsx

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
66
import { useClient } from '../contexts/Client'
77
import { useStack } from '../layouts/Stack'
88
import { ApView } from './ApView'
9-
import { NotFoundError } from '@concrnt/client'
9+
import { NotFoundError, type Document, type PolicyEntry } from '@concrnt/client'
1010
import { Schemas, semantics, type Timeline } from '@concrnt/worldlib'
1111
import { MdPlaylistAdd } from 'react-icons/md'
1212
import { Subscription } from '../components/Subscription'
@@ -48,11 +48,17 @@ export const Activitypub = () => {
4848

4949
const homeTimelineRegex = new RegExp(`^cckv://${client.ccid}/concrnt\\.world/profiles/([^/]+)/home-timeline$`)
5050
const inboxUri = apInboxKey(client.ccid)
51+
const allowWriters = 'https://policy.concrnt.world/t/allow-writers.json'
5152

5253
// ActivityPubタイムラインはリスト未登録だとknownCommunitiesに出ない上、
5354
// どのリストにも無いと受信した投稿を見る手段が無いので警告を出す
5455
const [pinnedLists] = useSubscribe(client.pinnedLists)
5556
const [inboxListed, setInboxListed] = useState(true)
57+
58+
// inboxのpolicyがブリッジの現serviceAccountIdを許可していないと配送が全て拒否される
59+
// (誤ったIDが広告された期間に設定画面を開くと壊れたpolicyが書き込まれる事故があった)
60+
const [inboxPolicyBroken, setInboxPolicyBroken] = useState(false)
61+
const [repairError, setRepairError] = useState(false)
5662
useEffect(() => {
5763
Promise.all(
5864
pinnedLists.map(async (pin) => {
@@ -99,7 +105,7 @@ export const Activitypub = () => {
99105
const defaultPolicy = {
100106
entries: [
101107
{
102-
url: 'https://policy.concrnt.world/t/allow-writers.json',
108+
url: allowWriters,
103109
params: {
104110
entities: [res.serviceAccountId]
105111
}
@@ -110,19 +116,40 @@ export const Activitypub = () => {
110116
client.api
111117
.getDocument<any>(inboxUri)
112118
.then((doc) => {
119+
// 配送時のrequesterはブリッジのserviceAccountIdなので、allow-writersの
120+
// entitiesに現在のIDが含まれていないと受信が全て拒否される
121+
const broken = !(
122+
doc.policy?.entries?.some(
123+
(e: PolicyEntry) =>
124+
e.url === allowWriters &&
125+
Array.isArray(e.params?.entities) &&
126+
e.params.entities.includes(res.serviceAccountId)
127+
) ?? false
128+
)
113129
// 旧クライアントが作ったinboxはuserTimelineスキーマで名前を持てないため、
114130
// communityTimelineスキーマに上書き修復する(ポリシーは維持)
115-
if (doc.schema === Schemas.communityTimeline && doc.value?.name) return
131+
if (doc.schema === Schemas.communityTimeline && doc.value?.name) {
132+
setInboxPolicyBroken(broken)
133+
return
134+
}
116135
console.log('Inbox has no metadata. repairing...')
117-
client.api.commit({
118-
kind: 'record' as const,
119-
key: inboxUri,
120-
author: client.ccid,
121-
schema: Schemas.communityTimeline,
122-
value: inboxValue,
123-
createdAt: new Date(),
124-
policy: doc.policy ?? defaultPolicy
125-
})
136+
client.api
137+
.commit({
138+
kind: 'record' as const,
139+
key: inboxUri,
140+
author: client.ccid,
141+
schema: Schemas.communityTimeline,
142+
value: inboxValue,
143+
createdAt: new Date(),
144+
policy: doc.policy ?? defaultPolicy
145+
})
146+
// 警告(=修復ボタン)はこのcommitの完了後に出す。in-flightの自動commitが
147+
// 修復ボタンのcommit後に着弾して誤policyを書き戻す競合を避けるため
148+
.then(() => setInboxPolicyBroken(doc.policy ? broken : false))
149+
.catch((err) => {
150+
console.log(err)
151+
setInboxPolicyBroken(broken)
152+
})
126153
})
127154
.catch((err) => {
128155
if (err instanceof NotFoundError) {
@@ -144,6 +171,57 @@ export const Activitypub = () => {
144171
})
145172
}, [])
146173

174+
const repairInboxPolicy = async (): Promise<void> => {
175+
setRepairError(false)
176+
try {
177+
// マウント時のIDは誤ったIDが広告されていた期間のものかもしれないので、押下時点で取り直す
178+
const info = await client.api.callConcrntApi<ApServerInfo>(
179+
client.server.domain,
180+
'net.concrnt.activitypub.info',
181+
{}
182+
)
183+
// staleキャッシュ由来の誤検知のまま上書きしないよう、書き込み前にno-cacheで確定させる
184+
let doc: Document<any> | null = null
185+
try {
186+
doc = await client.api.getDocument<any>(inboxUri, undefined, { cache: 'no-cache' })
187+
} catch (err) {
188+
if (!(err instanceof NotFoundError)) throw err
189+
}
190+
const alreadyOk =
191+
doc?.policy?.entries?.some(
192+
(e: PolicyEntry) =>
193+
e.url === allowWriters &&
194+
Array.isArray(e.params?.entities) &&
195+
e.params.entities.includes(info.serviceAccountId)
196+
) ?? false
197+
if (alreadyOk) {
198+
setInboxPolicyBroken(false)
199+
return
200+
}
201+
// allow-writers系entryは全て除去して正規形1本に置換(誤IDに書き込み権を残さない)。
202+
// restrict-readers等の他entryは温存
203+
const entries = (doc?.policy?.entries ?? []).filter((e: PolicyEntry) => e.url !== allowWriters)
204+
entries.push({ url: allowWriters, params: { entities: [info.serviceAccountId] } })
205+
const value =
206+
doc?.schema === Schemas.communityTimeline && doc.value?.name
207+
? doc.value
208+
: { name: 'ActivityPub', shortname: 'activitypub', description: 'ActivityPub home stream' }
209+
await client.api.commit({
210+
kind: 'record' as const,
211+
key: inboxUri,
212+
author: client.ccid,
213+
schema: Schemas.communityTimeline,
214+
value,
215+
createdAt: new Date(),
216+
policy: { entries }
217+
})
218+
setInboxPolicyBroken(false)
219+
} catch (err) {
220+
console.error('failed to repair inbox policy:', err)
221+
setRepairError(true)
222+
}
223+
}
224+
147225
const updateListenTimelines = () => {
148226
const listenTimelines = [
149227
...(listenHome ? [semantics.homeTimeline(client.ccid, listenProfile)] : []),
@@ -236,6 +314,21 @@ export const Activitypub = () => {
236314
</Button>
237315
</div>
238316
)}
317+
{inboxPolicyBroken && (
318+
<div style={{ display: 'flex', alignItems: 'center', gap: CssVar.space(1) }}>
319+
<Text variant="caption" style={{ color: 'red' }}>
320+
{t('inboxPolicyBroken')}
321+
</Text>
322+
<Button variant="text" busyChildren={t('repairing')} onClick={repairInboxPolicy}>
323+
{t('repair')}
324+
</Button>
325+
</div>
326+
)}
327+
{repairError && (
328+
<Text variant="caption" style={{ color: 'red' }}>
329+
{t('repairFailed')}
330+
</Text>
331+
)}
239332
<Divider />
240333
<Text>{t('forwardTimeline')}</Text>
241334
<Text>{t('forwardTimelineDesc')}</Text>

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@concrnt/client",
3-
"version": "2.0.4",
3+
"version": "2.0.5",
44
"description": "",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.js",

client/src/crypto.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ export const ValidateSignature = (body: string, signature: string, expectedKeyID
172172
}
173173

174174
export const LoadKey = (privateKey: string): KeyPair | null => {
175+
// ellipticは非hex文字列からも黙って鍵を導出してしまうため、ここで弾く
176+
if (!/^[0-9a-f]{64}$/i.test(privateKey)) return null
175177
try {
176178
const ellipsis = new Ec('secp256k1')
177179
const keyPair = ellipsis.keyFromPrivate(privateKey)

locales/en/translation.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,10 @@
345345
"forwardTimelineDesc": "Your posts to the selected timelines will be forwarded to ActivityPub. If none are set, your home timeline is used.",
346346
"inboxNotListed": "The ActivityPub timeline is not in any of your lists. Add it to a list to see incoming posts.",
347347
"addToList": "Add to list",
348+
"inboxPolicyBroken": "There is a problem with the inbox list permissions. Posts from ActivityPub cannot be delivered.",
349+
"repair": "Repair",
350+
"repairing": "Repairing...",
351+
"repairFailed": "Repair failed. Please try again later.",
348352
"update": "Update",
349353
"inquiry": "Inquiry"
350354
},

locales/ja/translation.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,10 @@
345345
"forwardTimelineDesc": "選択したタイムラインへの自分の投稿がActivityPubへ転送されます。未設定の場合はホームタイムラインが使用されます。",
346346
"inboxNotListed": "ActivityPubタイムラインがどのリストにも登録されていません。リストに追加しないと受信した投稿が表示されません。",
347347
"addToList": "リストに追加",
348+
"inboxPolicyBroken": "受信リストの権限設定に問題があり、ActivityPubからの投稿が届かない状態です。",
349+
"repair": "修復する",
350+
"repairing": "修復中...",
351+
"repairFailed": "修復に失敗しました。時間をおいて再度お試しください。",
348352
"update": "更新",
349353
"inquiry": "照会"
350354
},

ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@concrnt/ui",
3-
"version": "2.0.4",
3+
"version": "2.0.5",
44
"type": "module",
55
"files": [
66
"dist"

web/src/views/Activitypub.tsx

Lines changed: 105 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CssVar } from '../types/Theme'
33
import { useEffect, useState } from 'react'
44
import { useTranslation } from 'react-i18next'
55
import { useClient } from '../contexts/Client'
6-
import { NotFoundError } from '@concrnt/client'
6+
import { NotFoundError, type Document, type PolicyEntry } from '@concrnt/client'
77
import { useNavigate } from 'react-router-dom'
88
import { Schemas, semantics, type Timeline } from '@concrnt/worldlib'
99
import { 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

Comments
 (0)