kopia lustrzana https://github.com/cloudflare/wildebeest
subscription: remove hardcoded and switch to Int
rodzic
3d6f616785
commit
39046061db
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
Ładowanie…
Reference in New Issue