subscription: remove hardcoded and switch to Int

pull/286/head
Sven Sauleau 2023-02-15 11:39:15 +00:00
rodzic 3d6f616785
commit 39046061db
6 zmienionych plików z 274 dodań i 170 usunięć

Wyświetl plik

@ -31,8 +31,23 @@ export interface CreateRequest {
} }
export type Subscription = { export type Subscription = {
id: string // While the spec says to use a string as id (https://docs.joinmastodon.org/entities/WebPushSubscription/#id), Mastodon's android app decided to violate that (https://github.com/mastodon/mastodon-android/blob/master/mastodon/src/main/java/org/joinmastodon/android/model/PushSubscription.java#LL11).
id: number
gateway: PushSubscription gateway: PushSubscription
alerts: {
mention: boolean
status: boolean
reblog: boolean
follow: boolean
follow_request: boolean
favourite: boolean
poll: boolean
update: boolean
admin_sign_up: boolean
admin_report: boolean
}
policy: string
} }
export async function createSubscription( export async function createSubscription(
@ -41,39 +56,33 @@ export async function createSubscription(
client: Client, client: Client,
req: CreateRequest req: CreateRequest
): Promise<Subscription> { ): Promise<Subscription> {
const id = crypto.randomUUID()
const query = ` const query = `
INSERT INTO subscriptions (id, actor_id, client_id, endpoint, key_p256dh, key_auth, alert_mention, alert_status, alert_reblog, alert_follow, alert_follow_request, alert_favourite, alert_poll, alert_update, alert_admin_sign_up, alert_admin_report, policy) INSERT INTO subscriptions (actor_id, client_id, endpoint, key_p256dh, key_auth, alert_mention, alert_status, alert_reblog, alert_follow, alert_follow_request, alert_favourite, alert_poll, alert_update, alert_admin_sign_up, alert_admin_report, policy)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *
` `
const out = await db const row = await db
.prepare(query) .prepare(query)
.bind( .bind(
id,
actor.id.toString(), actor.id.toString(),
client.id, client.id,
req.subscription.endpoint, req.subscription.endpoint,
req.subscription.keys.p256dh, req.subscription.keys.p256dh,
req.subscription.keys.auth, req.subscription.keys.auth,
req.data.alerts.mention ? 1 : 0, req.data.alerts.mention === false ? 0 : 1,
req.data.alerts.status ? 1 : 0, req.data.alerts.status === false ? 0 : 1,
req.data.alerts.reblog ? 1 : 0, req.data.alerts.reblog === false ? 0 : 1,
req.data.alerts.follow ? 1 : 0, req.data.alerts.follow === false ? 0 : 1,
req.data.alerts.follow_request ? 1 : 0, req.data.alerts.follow_request === false ? 0 : 1,
req.data.alerts.favourite ? 1 : 0, req.data.alerts.favourite === false ? 0 : 1,
req.data.alerts.poll ? 1 : 0, req.data.alerts.poll === false ? 0 : 1,
req.data.alerts.update ? 1 : 0, req.data.alerts.update === false ? 0 : 1,
req.data.alerts.admin_sign_up ? 1 : 0, req.data.alerts.admin_sign_up === false ? 0 : 1,
req.data.alerts.admin_report ? 1 : 0, req.data.alerts.admin_report === false ? 0 : 1,
req.data.policy req.data.policy
) )
.run() .first<any>()
if (!out.success) { return subscriptionFromRow(row)
throw new Error('SQL error: ' + out.error)
}
return { id, gateway: req.subscription }
} }
export async function getSubscription(db: D1Database, actor: Actor, client: Client): Promise<Subscription | null> { export async function getSubscription(db: D1Database, actor: Actor, client: Client): Promise<Subscription | null> {
@ -121,6 +130,19 @@ function subscriptionFromRow(row: any): Subscription {
auth: row.key_auth, auth: row.key_auth,
}, },
}, },
alerts: {
mention: row.alert_mention === 1,
status: row.alert_status === 1,
reblog: row.alert_reblog === 1,
follow: row.alert_follow === 1,
follow_request: row.alert_follow_request === 1,
favourite: row.alert_favourite === 1,
poll: row.alert_poll === 1,
update: row.alert_update === 1,
admin_sign_up: row.alert_admin_sign_up === 1,
admin_report: row.alert_admin_report === 1,
},
policy: row.policy,
} }
} }

Wyświetl plik

@ -1,5 +1,4 @@
import { strict as assert } from 'node:assert/strict' import { strict as assert } from 'node:assert/strict'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Env } from 'wildebeest/backend/src/types/env' import type { Env } from 'wildebeest/backend/src/types/env'
import * as v1_instance from 'wildebeest/functions/api/v1/instance' import * as v1_instance from 'wildebeest/functions/api/v1/instance'
import * as v2_instance from 'wildebeest/functions/api/v2/instance' import * as v2_instance from 'wildebeest/functions/api/v2/instance'
@ -7,24 +6,11 @@ import * as apps from 'wildebeest/functions/api/v1/apps'
import * as custom_emojis from 'wildebeest/functions/api/v1/custom_emojis' import * as custom_emojis from 'wildebeest/functions/api/v1/custom_emojis'
import * as mutes from 'wildebeest/functions/api/v1/mutes' import * as mutes from 'wildebeest/functions/api/v1/mutes'
import * as blocks from 'wildebeest/functions/api/v1/blocks' import * as blocks from 'wildebeest/functions/api/v1/blocks'
import { makeDB, assertCORS, assertJSON, assertCache, createTestClient } from './utils' import { makeDB, assertCORS, assertJSON, assertCache, generateVAPIDKeys } from './utils'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { createSubscription } from '../src/mastodon/subscription'
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats' import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
const userKEK = 'test_kek'
const domain = 'cloudflare.com' const domain = 'cloudflare.com'
async function generateVAPIDKeys(): Promise<JWK> {
const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
])) as CryptoKeyPair
const jwk = (await crypto.subtle.exportKey('jwk', keyPair.privateKey)) as JWK
return jwk
}
describe('Mastodon APIs', () => { describe('Mastodon APIs', () => {
describe('instance', () => { describe('instance', () => {
type Data = { type Data = {
@ -158,113 +144,6 @@ describe('Mastodon APIs', () => {
}) })
}) })
describe('subscriptions', () => {
test('get non existing subscription', async () => {
const db = await makeDB()
const req = new Request('https://example.com')
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const res = await subscription.handleGetRequest(db, req, connectedActor, client.id)
assert.equal(res.status, 404)
})
test('get existing subscription', async () => {
const db = await makeDB()
const req = new Request('https://example.com')
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {},
policy: 'all',
},
}
await createSubscription(db, connectedActor, client, data)
const res = await subscription.handleGetRequest(db, req, connectedActor, client.id)
assert.equal(res.status, 200)
const out = await res.json<any>()
assert.equal(typeof out.id, 'number')
assert.equal(out.endpoint, data.subscription.endpoint)
})
test('create subscription', async () => {
const db = await makeDB()
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const vapidKeys = await generateVAPIDKeys()
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {},
policy: 'all',
},
}
const req = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const row: any = await db.prepare('SELECT * FROM subscriptions').first()
assert.equal(row.actor_id, connectedActor.id.toString())
assert.equal(row.client_id, client.id)
assert.equal(row.endpoint, data.subscription.endpoint)
})
test('create subscriptions only creates one', async () => {
const db = await makeDB()
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const vapidKeys = await generateVAPIDKeys()
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {},
policy: 'all',
},
}
await createSubscription(db, connectedActor, client, data)
const req = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first<{ count: number }>()
assert.equal(count, 1)
})
})
test('mutes returns an empty array', async () => { test('mutes returns an empty array', async () => {
const res = await mutes.onRequest() const res = await mutes.onRequest()
assert.equal(res.status, 200) assert.equal(res.status, 200)

Wyświetl plik

@ -0,0 +1,171 @@
import { createSubscription } from '../../src/mastodon/subscription'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { strict as assert } from 'node:assert/strict'
import { makeDB, createTestClient, generateVAPIDKeys } from '../utils'
import * as subscription from 'wildebeest/functions/api/v1/push/subscription'
const userKEK = 'test_kek21'
const domain = 'cloudflare.com'
describe('Mastodon APIs', () => {
describe('subscriptions', () => {
test('get non existing subscription', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const req = new Request('https://example.com')
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const res = await subscription.handleGetRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 404)
})
test('get existing subscription', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const req = new Request('https://example.com')
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {
follow: false,
favourite: true,
reblog: false,
poll: true,
},
policy: 'followed',
},
}
await createSubscription(db, connectedActor, client, data)
const res = await subscription.handleGetRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const out = await res.json<any>()
assert.equal(typeof out.id, 'number')
assert.equal(out.endpoint, data.subscription.endpoint)
assert.equal(out.alerts.follow, false)
assert.equal(out.alerts.favourite, true)
assert.equal(out.alerts.reblog, false)
assert.equal(out.alerts.poll, true)
assert.equal(out.policy, 'followed')
})
test('create subscription', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {
poll: false,
status: true,
},
policy: 'all',
},
}
const req = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const out = await res.json<any>()
assert.equal(out.alerts.mention, true)
assert.equal(out.alerts.status, true) // default to true
assert.equal(out.alerts.poll, false)
const row: any = await db.prepare('SELECT * FROM subscriptions').first()
assert.equal(row.actor_id, connectedActor.id.toString())
assert.equal(row.client_id, client.id)
assert.equal(row.endpoint, data.subscription.endpoint)
assert.equal(row.alert_poll, 0)
assert.equal(row.alert_mention, 1)
assert.equal(row.alert_status, 1) // default to true
})
test('create subscriptions only creates one', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys()
const client = await createTestClient(db)
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {},
policy: 'all',
},
}
await createSubscription(db, connectedActor, client, data)
const req = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify(data),
})
const res = await subscription.handlePostRequest(db, req, connectedActor, client.id, vapidKeys)
assert.equal(res.status, 200)
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first<{ count: number }>()
assert.equal(count, 1)
})
test('subscriptions auto increment', async () => {
const db = await makeDB()
const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const data: any = {
subscription: {
endpoint: 'https://endpoint.com',
keys: {
p256dh: 'p256dh',
auth: 'auth',
},
},
data: {
alerts: {},
policy: 'all',
},
}
const client1 = await createTestClient(db)
const sub1 = await createSubscription(db, connectedActor, client1, data)
assert.equal(sub1.id, 1)
const client2 = await createTestClient(db)
const sub2 = await createSubscription(db, connectedActor, client2, data)
assert.equal(sub2.id, 2)
const client3 = await createTestClient(db)
const sub3 = await createSubscription(db, connectedActor, client3, data)
assert.equal(sub3.id, 3)
})
})
})

Wyświetl plik

@ -1,4 +1,5 @@
import { strict as assert } from 'node:assert/strict' import { strict as assert } from 'node:assert/strict'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Cache } from 'wildebeest/backend/src/cache' import type { Cache } from 'wildebeest/backend/src/cache'
import type { Queue } from 'wildebeest/backend/src/types/queue' import type { Queue } from 'wildebeest/backend/src/types/queue'
import { createClient } from 'wildebeest/backend/src/mastodon/client' import { createClient } from 'wildebeest/backend/src/mastodon/client'
@ -117,3 +118,12 @@ export function isUUID(v: string): boolean {
} }
return true return true
} }
export async function generateVAPIDKeys(): Promise<JWK> {
const keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
'sign',
'verify',
])) as CryptoKeyPair
const jwk = (await crypto.subtle.exportKey('jwk', keyPair.privateKey)) as JWK
return jwk
}

Wyświetl plik

@ -11,7 +11,7 @@ import * as errors from 'wildebeest/backend/src/errors'
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription' import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => { export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId) return handleGetRequest(env.DATABASE, request, data.connectedActor, data.clientId, getVAPIDKeys(env))
} }
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => { export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
@ -23,7 +23,13 @@ const headers = {
'content-type': 'application/json; charset=utf-8', 'content-type': 'application/json; charset=utf-8',
} }
export async function handleGetRequest(db: D1Database, request: Request, connectedActor: Actor, clientId: string) { export async function handleGetRequest(
db: D1Database,
request: Request,
connectedActor: Actor,
clientId: string,
vapidKeys: JWK
) {
const client = await getClientById(db, clientId) const client = await getClientById(db, clientId)
if (client === null) { if (client === null) {
return errors.clientUnknown() return errors.clientUnknown()
@ -35,20 +41,14 @@ export async function handleGetRequest(db: D1Database, request: Request, connect
return new Response('', { status: 404 }) return new Response('', { status: 404 })
} }
const res = { const vapidKey = VAPIDPublicKey(vapidKeys)
id: 4,
endpoint: subscription.gateway.endpoint,
alerts: {
follow: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
},
policy: 'all',
// FIXME: stub value const res = {
server_key: 'TODO', id: subscription.id,
endpoint: subscription.gateway.endpoint,
alerts: subscription.alerts,
policy: subscription.policy,
server_key: vapidKey,
} }
return new Response(JSON.stringify(res), { headers }) return new Response(JSON.stringify(res), { headers })
@ -77,16 +77,10 @@ export async function handlePostRequest(
const vapidKey = VAPIDPublicKey(vapidKeys) const vapidKey = VAPIDPublicKey(vapidKeys)
const res = { const res = {
id: 4, id: subscription.id,
endpoint: data.subscription.endpoint, endpoint: subscription.gateway.endpoint,
alerts: { alerts: subscription.alerts,
follow: true, policy: subscription.policy,
favourite: true,
reblog: true,
mention: true,
poll: true,
},
policy: 'all',
server_key: vapidKey, server_key: vapidKey,
} }

Wyświetl plik

@ -0,0 +1,28 @@
-- Migration number: 0007 2023-02-15T11:01:46.585Z
DROP table subscriptions;
CREATE TABLE subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL,
client_id TEXT NOT NULL,
endpoint TEXT NULL NOT NULL,
key_p256dh TEXT NOT NULL,
key_auth TEXT NOT NULL,
alert_mention INTEGER NOT NULL,
alert_status INTEGER NOT NULL,
alert_reblog INTEGER NOT NULL,
alert_follow INTEGER NOT NULL,
alert_follow_request INTEGER NOT NULL,
alert_favourite INTEGER NOT NULL,
alert_poll INTEGER NOT NULL,
alert_update INTEGER NOT NULL,
alert_admin_sign_up INTEGER NOT NULL,
alert_admin_report INTEGER NOT NULL,
policy TEXT NOT NULL,
cdate DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
UNIQUE(actor_id, client_id)
FOREIGN KEY(actor_id) REFERENCES actors(id),
FOREIGN KEY(client_id) REFERENCES clients(id)
);