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 = {
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
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(
@ -41,39 +56,33 @@ export async function createSubscription(
client: Client,
req: CreateRequest
): Promise<Subscription> {
const id = crypto.randomUUID()
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *
`
const out = await db
const row = await db
.prepare(query)
.bind(
id,
actor.id.toString(),
client.id,
req.subscription.endpoint,
req.subscription.keys.p256dh,
req.subscription.keys.auth,
req.data.alerts.mention ? 1 : 0,
req.data.alerts.status ? 1 : 0,
req.data.alerts.reblog ? 1 : 0,
req.data.alerts.follow ? 1 : 0,
req.data.alerts.follow_request ? 1 : 0,
req.data.alerts.favourite ? 1 : 0,
req.data.alerts.poll ? 1 : 0,
req.data.alerts.update ? 1 : 0,
req.data.alerts.admin_sign_up ? 1 : 0,
req.data.alerts.admin_report ? 1 : 0,
req.data.alerts.mention === false ? 0 : 1,
req.data.alerts.status === false ? 0 : 1,
req.data.alerts.reblog === false ? 0 : 1,
req.data.alerts.follow === false ? 0 : 1,
req.data.alerts.follow_request === false ? 0 : 1,
req.data.alerts.favourite === false ? 0 : 1,
req.data.alerts.poll === false ? 0 : 1,
req.data.alerts.update === false ? 0 : 1,
req.data.alerts.admin_sign_up === false ? 0 : 1,
req.data.alerts.admin_report === false ? 0 : 1,
req.data.policy
)
.run()
if (!out.success) {
throw new Error('SQL error: ' + out.error)
}
return { id, gateway: req.subscription }
.first<any>()
return subscriptionFromRow(row)
}
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,
},
},
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 type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Env } from 'wildebeest/backend/src/types/env'
import * as v1_instance from 'wildebeest/functions/api/v1/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 mutes from 'wildebeest/functions/api/v1/mutes'
import * as blocks from 'wildebeest/functions/api/v1/blocks'
import { makeDB, assertCORS, assertJSON, assertCache, createTestClient } 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 { makeDB, assertCORS, assertJSON, assertCache, generateVAPIDKeys } from './utils'
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
const userKEK = 'test_kek'
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('instance', () => {
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 () => {
const res = await mutes.onRequest()
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 type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Cache } from 'wildebeest/backend/src/cache'
import type { Queue } from 'wildebeest/backend/src/types/queue'
import { createClient } from 'wildebeest/backend/src/mastodon/client'
@ -117,3 +118,12 @@ export function isUUID(v: string): boolean {
}
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'
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 }) => {
@ -23,7 +23,13 @@ const headers = {
'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)
if (client === null) {
return errors.clientUnknown()
@ -35,20 +41,14 @@ export async function handleGetRequest(db: D1Database, request: Request, connect
return new Response('', { status: 404 })
}
const res = {
id: 4,
endpoint: subscription.gateway.endpoint,
alerts: {
follow: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
},
policy: 'all',
const vapidKey = VAPIDPublicKey(vapidKeys)
// FIXME: stub value
server_key: 'TODO',
const res = {
id: subscription.id,
endpoint: subscription.gateway.endpoint,
alerts: subscription.alerts,
policy: subscription.policy,
server_key: vapidKey,
}
return new Response(JSON.stringify(res), { headers })
@ -77,16 +77,10 @@ export async function handlePostRequest(
const vapidKey = VAPIDPublicKey(vapidKeys)
const res = {
id: 4,
endpoint: data.subscription.endpoint,
alerts: {
follow: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
},
policy: 'all',
id: subscription.id,
endpoint: subscription.gateway.endpoint,
alerts: subscription.alerts,
policy: subscription.policy,
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)
);