improve be (+functions) linting:

- enable no-unsafe-call for be/functions linting
 - enable backend await-thenable eslint rule
 - enable backend no-misused-promises eslint rule
pull/75/head
Dario Piotrowicz 2023-01-11 15:45:07 +00:00
rodzic c15f6bec5e
commit 3738c7dda1
22 zmienionych plików z 230 dodań i 132 usunięć

Wyświetl plik

@ -18,6 +18,10 @@ module.exports = {
'@typescript-eslint/no-unused-vars': 'error',
'no-console': 'off',
'no-constant-condition': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': 'error',
/*
Note: the following rules have been set to off so that linting
can pass with the current code, but we need to gradually
@ -25,13 +29,9 @@ module.exports = {
*/
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'@typescript-eslint/await-thenable': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-inferrable-types': 'off',

Wyświetl plik

@ -121,7 +121,7 @@ export const generateValidator =
const unroundedSecondsSinceEpoch = Date.now() / 1000
const payloadObj = JSON.parse(textDecoder.decode(base64URLDecode(payload)))
const payloadObj = JSON.parse(textDecoder.decode(base64URLDecode(payload))) as JWTPayload
// For testing disable JWT checks.
// Ideally we match the production behavior in testing but that

Wyświetl plik

@ -29,7 +29,7 @@ function extractID(domain: string, s: string | URL): string {
return s.toString().replace(`https://${domain}/ap/users/`, '')
}
export function makeGetObjectAsId(activity: Activity): Function {
export function makeGetObjectAsId(activity: Activity) {
return () => {
let url: any = null
if (activity.object.id !== undefined) {
@ -55,7 +55,7 @@ export function makeGetObjectAsId(activity: Activity): Function {
}
}
export function makeGetActorAsId(activity: Activity): Function {
export function makeGetActorAsId(activity: Activity) {
return () => {
let url: any = null
if (activity.actor.id !== undefined) {

Wyświetl plik

@ -154,7 +154,7 @@ export async function getObjectByMastodonId(db: D1Database, id: UUID): Promise<O
return getObjectBy(db, 'mastodon_id', id)
}
export async function getObjectBy(db: D1Database, key: string, value: string): Promise<Object | null> {
export async function getObjectBy(db: D1Database, key: string, value: string) {
const query = `
SELECT *
FROM objects

Wyświetl plik

@ -1,3 +1,5 @@
import type { Env } from 'wildebeest/consumer/src'
const CACHE_DO_NAME = 'cachev1'
export interface Cache {
@ -5,7 +7,7 @@ export interface Cache {
put<T>(key: string, value: T): Promise<void>
}
export function cacheFromEnv(env: any): Cache {
export function cacheFromEnv(env: Env): Cache {
return {
async get<T>(key: string): Promise<T | null> {
const id = env.DO_CACHE.idFromName(CACHE_DO_NAME)

Wyświetl plik

@ -8,7 +8,11 @@ import { getPersonById } from 'wildebeest/backend/src/activitypub/actors'
import type { WebPushInfos, WebPushMessage } from 'wildebeest/backend/src/webpush/webpushinfos'
import { WebPushResult } from 'wildebeest/backend/src/webpush/webpushinfos'
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
import type { NotificationType, Notification } from 'wildebeest/backend/src/types/notification'
import type {
NotificationType,
Notification,
NotificationsQueryResult,
} from 'wildebeest/backend/src/types/notification'
import { getSubscriptionForAllClients } from 'wildebeest/backend/src/mastodon/subscription'
import type { Cache } from 'wildebeest/backend/src/cache'
@ -24,10 +28,10 @@ export async function createNotification(
VALUES (?, ?, ?, ?)
RETURNING id
`
const row: { id: string } = await db
const row = await db
.prepare(query)
.bind(type, actor.id.toString(), fromActor.id.toString(), obj.id.toString())
.first()
.first<{ id: string }>()
return row.id
}
@ -39,7 +43,7 @@ export async function insertFollowNotification(db: D1Database, actor: Actor, fro
VALUES (?, ?, ?)
RETURNING id
`
const row: { id: string } = await db.prepare(query).bind(type, actor.id.toString(), fromActor.id.toString()).first()
const row = await db.prepare(query).bind(type, actor.id.toString(), fromActor.id.toString()).first<{ id: string }>()
return row.id
}
@ -187,7 +191,7 @@ export async function getNotifications(db: D1Database, actor: Actor, domain: str
`
const stmt = db.prepare(query).bind(actor.id.toString())
const { results, success, error } = await stmt.all()
const { results, success, error } = await stmt.all<NotificationsQueryResult>()
if (!success) {
throw new Error('SQL error: ' + error)
}
@ -198,7 +202,7 @@ export async function getNotifications(db: D1Database, actor: Actor, domain: str
}
for (let i = 0, len = results.length; i < len; i++) {
const result = results[i] as any
const result = results[i]
const properties = JSON.parse(result.properties)
const notifFromActorId = new URL(result.notif_from_actor_id)

Wyświetl plik

@ -1,5 +1,6 @@
import type { MastodonAccount } from 'wildebeest/backend/src/types/account'
import type { MastodonStatus } from 'wildebeest/backend/src/types/status'
import type { ObjectsRow } from './objects'
export type NotificationType =
| 'mention'
@ -20,3 +21,12 @@ export type Notification = {
account: MastodonAccount
status?: MastodonStatus
}
export interface NotificationsQueryResult extends ObjectsRow {
type: NotificationType
original_actor_id: URL
notif_from_actor_id: URL
notif_cdate: string
notif_id: URL
from_actor_id: string
}

Wyświetl plik

@ -0,0 +1,6 @@
export interface ObjectsRow {
properties: string
mastodon_id: string
id: URL
cdate: string
}

Wyświetl plik

@ -26,10 +26,8 @@ export async function readBody<T>(request: Request): Promise<T> {
// The `key[]` notiation is used when sending an array of values.
const key2 = key.replace('[]', '')
if (out[key2] === undefined) {
out[key2] = []
}
out[key2].push(value)
const outArr: unknown[] = (out[key2] ??= [])
outArr.push(value)
} else {
out[key] = value
}

Wyświetl plik

@ -1,4 +1,3 @@
// @ts-nocheck
// Copyright 2012 Joyent, Inc. All rights reserved.
import { HEADER, HttpSignatureError, InvalidAlgorithmError, validateAlgorithm } from './utils'
@ -61,6 +60,9 @@ export type ParsedSignature = {
keyId: string
signingString: string
algorithm: string
scheme: string
params: Record<string, string | string[] | number>
opaque: string
}
///--- Exported API
@ -141,10 +143,14 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
let tmpName = ''
let tmpValue = ''
const parsed = {
const parsed: ParsedSignature = {
scheme: authz === request.headers.get(HEADER.SIG) ? 'Signature' : '',
params: {},
signingString: '',
signature: '',
keyId: '',
algorithm: '',
opaque: '',
}
for (i = 0; i < authz.length; i++) {
@ -234,14 +240,16 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
}
}
let parsedHeaders: string[] = []
if (!parsed.params.headers || parsed.params.headers === '') {
if (request.headers.has('x-date')) {
parsed.params.headers = ['x-date']
parsedHeaders = ['x-date']
} else {
parsed.params.headers = ['date']
parsedHeaders = ['date']
}
} else {
parsed.params.headers = parsed.params.headers.split(' ')
} else if (typeof parsed.params.headers === 'string') {
parsedHeaders = parsed.params.headers.split(' ')
}
// Minimally validate the parsed object
@ -253,13 +261,13 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
if (!parsed.params.signature) throw new InvalidHeaderError('signature was not specified')
if (['date', 'x-date', '(created)'].every((hdr) => parsed.params.headers.indexOf(hdr) < 0)) {
if (['date', 'x-date', '(created)'].every((hdr) => parsedHeaders.indexOf(hdr) < 0)) {
throw new MissingHeaderError('no signed date header')
}
// Check the algorithm against the official list
try {
validateAlgorithm(parsed.params.algorithm, 'rsa')
validateAlgorithm(parsed.params.algorithm as string, 'rsa')
} catch (e) {
if (e instanceof InvalidAlgorithmError)
throw new InvalidParamsError(parsed.params.algorithm + ' is not ' + 'supported')
@ -267,9 +275,9 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
}
// Build the signingString
for (i = 0; i < parsed.params.headers.length; i++) {
const h = parsed.params.headers[i].toLowerCase()
parsed.params.headers[i] = h
for (i = 0; i < parsedHeaders.length; i++) {
const h = parsedHeaders[i].toLowerCase()
parsedHeaders[i] = h
if (h === 'request-line') {
if (!options.strict) {
@ -292,6 +300,7 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
} else if (h === '(opaque)') {
const opaque = parsed.params.opaque
if (opaque === undefined) {
//@ts-expect-error -- authzHeaderName doesn't exist TOFIX
throw new MissingHeaderError('opaque param was not in the ' + authzHeaderName + ' header')
}
parsed.signingString += '(opaque): ' + opaque
@ -305,17 +314,17 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
parsed.signingString += h + ': ' + value
}
if (i + 1 < parsed.params.headers.length) parsed.signingString += '\n'
if (i + 1 < parsedHeaders.length) parsed.signingString += '\n'
}
// Check against the constraints
let date
let skew
if (request.headers.date || request.headers.has('x-date')) {
if (request.headers.get('date') || request.headers.has('x-date')) {
if (request.headers.has('x-date')) {
date = new Date(request.headers.get('x-date') as string)
} else {
date = new Date(request.headers.date)
date = new Date(request.headers.get('date') as string)
}
const now = new Date()
skew = Math.abs(now.getTime() - date.getTime())
@ -325,7 +334,7 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
}
}
if (parsed.params.created) {
if (parsed.params.created && typeof parsed.params.created === 'number') {
skew = parsed.params.created - Math.floor(Date.now() / 1000)
if (skew > options.clockSkew) {
throw new ExpiredRequestError(
@ -340,7 +349,7 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
}
}
if (parsed.params.expires) {
if (parsed.params.expires && typeof parsed.params.expires === 'number') {
const expiredSince = Math.floor(Date.now() / 1000) - parsed.params.expires
if (expiredSince > options.clockSkew) {
throw new ExpiredRequestError(
@ -352,14 +361,17 @@ export function parseRequest(request: Request, options?: Options): ParsedSignatu
headers.forEach(function (hdr) {
// Remember that we already checked any headers in the params
// were in the request, so if this passes we're good.
if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0)
if (parsedHeaders.indexOf(hdr.toLowerCase()) < 0) {
throw new MissingHeaderError(hdr + ' was not a signed header')
}
})
parsed.params.algorithm = parsed.params.algorithm.toLowerCase()
parsed.algorithm = parsed.params.algorithm.toUpperCase()
parsed.keyId = parsed.params.keyId
parsed.opaque = parsed.params.opaque
parsed.signature = parsed.params.signature
const algorithm = parsed.params.algorithm as string
parsed.params.algorithm = algorithm.toLowerCase()
parsed.algorithm = algorithm.toUpperCase()
parsed.keyId = parsed.params.keyId as string
parsed.opaque = parsed.params.opaque as string
parsed.signature = parsed.params.signature as string
parsed.params.headers = parsedHeaders
return parsed
}

Wyświetl plik

@ -21,16 +21,17 @@ describe('ActivityPub', () => {
beforeEach(() => {
receivedActivity = null
globalThis.fetch = async (input: any) => {
if (input.url === `https://${domain}/ap/users/sven2/inbox`) {
assert.equal(input.method, 'POST')
const data = await input.json()
globalThis.fetch = async (input: RequestInfo) => {
const request = new Request(input)
if (request.url === `https://${domain}/ap/users/sven2/inbox`) {
assert.equal(request.method, 'POST')
const data = await request.json()
receivedActivity = data
console.log({ receivedActivity })
return new Response('')
}
throw new Error('unexpected request to ' + input.url)
throw new Error('unexpected request to ' + request.url)
}
})
@ -51,14 +52,17 @@ describe('ActivityPub', () => {
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
.bind(actor2.id.toString())
.first()
.first<{
target_actor_id: object
state: string
}>()
assert(row)
assert.equal(row.target_actor_id.toString(), actor.id.toString())
assert.equal(row.state, 'accepted')
assert(receivedActivity)
assert.equal(receivedActivity.type, 'Accept')
assert.equal(receivedActivity.actor.toString(), actor.id.toString())
assert.equal((receivedActivity.actor as object).toString(), actor.id.toString())
assert.equal(receivedActivity.object.actor, activity.actor)
assert.equal(receivedActivity.object.type, activity.type)
})
@ -144,7 +148,11 @@ describe('ActivityPub', () => {
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
const entry = await db.prepare('SELECT * FROM actor_notifications').first<{
type: string
actor_id: object
from_actor_id: object
}>()
assert.equal(entry.type, 'follow')
assert.equal(entry.actor_id.toString(), actor.id.toString())
assert.equal(entry.from_actor_id.toString(), actor2.id.toString())
@ -167,7 +175,7 @@ describe('ActivityPub', () => {
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
// Even if we followed multiple times, only one row should be present.
const { count } = await db.prepare(`SELECT count(*) as count FROM actor_following`).first()
const { count } = await db.prepare(`SELECT count(*) as count FROM actor_following`).first<{ count: number }>()
assert.equal(count, 1)
})
})

Wyświetl plik

@ -6,6 +6,7 @@ import { cacheObject, getObjectById } from 'wildebeest/backend/src/activitypub/o
import { addFollowing } from 'wildebeest/backend/src/mastodon/follow'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { ObjectsRow } from 'wildebeest/backend/src/types/objects'
const adminEmail = 'admin@example.com'
const domain = 'cloudflare.com'
@ -29,7 +30,10 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
const entry = await db.prepare('SELECT * FROM actor_reblogs').first<{
actor_id: URL
object_id: URL
}>()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
@ -48,7 +52,11 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
const entry = await db.prepare('SELECT * FROM actor_notifications').first<{
type: string
actor_id: URL
from_actor_id: URL
}>()
assert(entry)
assert.equal(entry.type, 'reblog')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
@ -71,7 +79,7 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
const entry = await db.prepare('SELECT * FROM actor_favourites').first<{ actor_id: URL; object_id: URL }>()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
@ -90,7 +98,11 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
const entry = await db.prepare('SELECT * FROM actor_notifications').first<{
type: string
actor_id: URL
from_actor_id: URL
}>()
assert.equal(entry.type, 'favourite')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
@ -110,7 +122,10 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
const entry = await db.prepare('SELECT * FROM actor_favourites').first<{
actor_id: URL
object_id: URL
}>()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
@ -145,7 +160,10 @@ describe('ActivityPub', () => {
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
.bind(actor.id.toString())
.first()
.first<{
target_actor_id: string
state: string
}>()
assert(row)
assert.equal(row.target_actor_id, 'https://' + domain + '/ap/users/sven2')
assert.equal(row.state, 'accepted')
@ -204,7 +222,7 @@ describe('ActivityPub', () => {
const entry = await db
.prepare('SELECT objects.* FROM inbox_objects INNER JOIN objects ON objects.id=inbox_objects.object_id')
.first()
.first<ObjectsRow>()
const properties = JSON.parse(entry.properties)
assert.equal(properties.content, 'test note')
})
@ -241,7 +259,10 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM outbox_objects WHERE actor_id=?').bind(remoteActorId).first()
const entry = await db
.prepare('SELECT * FROM outbox_objects WHERE actor_id=?')
.bind(remoteActorId)
.first<{ actor_id: string }>()
assert.equal(entry.actor_id, remoteActorId)
})
@ -263,7 +284,11 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
const entry = await db.prepare('SELECT * FROM actor_notifications').first<{
type: string
actor_id: URL
from_actor_id: URL
}>()
assert(entry)
assert.equal(entry.type, 'mention')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
@ -303,7 +328,11 @@ describe('ActivityPub', () => {
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
}
const entry = await db.prepare('SELECT * FROM actor_replies').first()
const entry = await db.prepare('SELECT * FROM actor_replies').first<{
actor_id: string
object_id: string
in_reply_to_object_id: string
}>()
assert.equal(entry.actor_id, actor.id.toString().toString())
const obj: any = await getObjectById(db, entry.object_id)
@ -319,7 +348,7 @@ describe('ActivityPub', () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
const activity = {
type: 'Create',
actor: actor.id.toString(),
to: ['some actor'],
@ -332,7 +361,7 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db.prepare('SELECT * FROM outbox_objects').first()
const row = await db.prepare('SELECT * FROM outbox_objects').first<{ target: string }>()
assert.equal(row.target, 'some actor')
})
@ -355,7 +384,7 @@ describe('ActivityPub', () => {
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db.prepare(`SELECT * from objects`).first()
const row = await db.prepare(`SELECT * from objects`).first<ObjectsRow>()
const { content, name } = JSON.parse(row.properties)
assert.equal(
content,
@ -454,7 +483,7 @@ describe('ActivityPub', () => {
const updatedObject = await db
.prepare('SELECT * FROM objects WHERE original_object_id=?')
.bind(object.id)
.first()
.first<ObjectsRow>()
assert(updatedObject)
assert.equal(JSON.parse(updatedObject.properties).content, newObject.content)
})
@ -500,7 +529,10 @@ describe('ActivityPub', () => {
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const object = await db.prepare('SELECT * FROM objects').first()
const object = await db.prepare('SELECT * FROM objects').first<{
type: string
original_actor_id: string
}>()
assert(object)
assert.equal(object.type, 'Note')
assert.equal(object.original_actor_id, remoteActorId)
@ -508,7 +540,7 @@ describe('ActivityPub', () => {
const outbox_object = await db
.prepare('SELECT * FROM outbox_objects WHERE actor_id=?')
.bind(remoteActorId)
.first()
.first<{ actor_id: string }>()
assert(outbox_object)
assert.equal(outbox_object.actor_id, remoteActorId)
})

Wyświetl plik

@ -26,6 +26,17 @@ async function generateVAPIDKeys(): Promise<JWK> {
describe('Mastodon APIs', () => {
describe('instance', () => {
type Data = {
rules: unknown[]
uri: string
title: string
email: string
description: string
version: string
domain: string
contact: { email: string }
}
test('return the instance infos v1', async () => {
const env = {
INSTANCE_TITLE: 'a',
@ -39,7 +50,7 @@ describe('Mastodon APIs', () => {
assertJSON(res)
{
const data = await res.json<any>()
const data = await res.json<Data>()
assert.equal(data.rules.length, 0)
assert.equal(data.uri, domain)
assert.equal(data.title, 'a')
@ -77,7 +88,7 @@ describe('Mastodon APIs', () => {
assertJSON(res)
{
const data = await res.json<any>()
const data = await res.json<Data>()
assert.equal(data.rules.length, 0)
assert.equal(data.domain, domain)
assert.equal(data.title, 'a')
@ -248,7 +259,7 @@ describe('Mastodon APIs', () => {
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()
const { count } = await db.prepare('SELECT count(*) as count FROM subscriptions').first<{ count: number }>()
assert.equal(count, 1)
})
})

Wyświetl plik

@ -169,7 +169,7 @@ describe('Mastodon APIs', () => {
globalThis.fetch = async (input: RequestInfo, data: any) => {
if (input === 'https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT_ID/images/v1') {
assert.equal(data.method, 'POST')
const file: any = data.body.get('file')
const file: any = (data.body as { get: (str: string) => any }).get('file')
return new Response(
JSON.stringify({
success: true,
@ -333,7 +333,7 @@ describe('Mastodon APIs', () => {
assert.equal(data.following_count, 2)
assert.equal(data.statuses_count, 1)
assert(isUrlValid(data.url))
assert(data.url.includes(domain))
assert((data.url as string).includes(domain))
})
test('get local actor statuses', async () => {
@ -561,7 +561,7 @@ describe('Mastodon APIs', () => {
// Statuses were imported locally and once was a reblog of an already
// existing local object.
const row = await db.prepare(`SELECT count(*) as count FROM objects`).first()
const row: { count: number } = await db.prepare(`SELECT count(*) as count FROM objects`).first()
assert.equal(row.count, 2)
})
@ -640,7 +640,7 @@ describe('Mastodon APIs', () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
globalThis.fetch = async (input: any) => {
globalThis.fetch = async (input: RequestInfo) => {
if (input.toString() === 'https://example.com/.well-known/webfinger?resource=acct%3Asven%40example.com') {
return new Response(
JSON.stringify({
@ -718,7 +718,7 @@ describe('Mastodon APIs', () => {
test('get local actor followers', async () => {
globalThis.fetch = async (input: any) => {
if (input.toString() === 'https://' + domain + '/ap/users/sven2') {
if ((input as object).toString() === 'https://' + domain + '/ap/users/sven2') {
return new Response(
JSON.stringify({
id: 'https://example.com/actor',
@ -746,7 +746,7 @@ describe('Mastodon APIs', () => {
test('get local actor following', async () => {
globalThis.fetch = async (input: any) => {
if (input.toString() === 'https://' + domain + '/ap/users/sven2') {
if ((input as object).toString() === 'https://' + domain + '/ap/users/sven2') {
return new Response(
JSON.stringify({
id: 'https://example.com/foo',
@ -776,7 +776,7 @@ describe('Mastodon APIs', () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
globalThis.fetch = async (input: any) => {
globalThis.fetch = async (input: RequestInfo) => {
if (input.toString() === 'https://example.com/.well-known/webfinger?resource=acct%3Asven%40example.com') {
return new Response(
JSON.stringify({
@ -952,11 +952,9 @@ describe('Mastodon APIs', () => {
beforeEach(() => {
receivedActivity = null
globalThis.fetch = async (input: any) => {
if (
input.toString() ===
'https://' + domain + '/.well-known/webfinger?resource=acct%3Aactor%40' + domain + ''
) {
globalThis.fetch = async (input: RequestInfo) => {
const request = new Request(input)
if (request.url === 'https://' + domain + '/.well-known/webfinger?resource=acct%3Aactor%40' + domain + '') {
return new Response(
JSON.stringify({
links: [
@ -970,7 +968,7 @@ describe('Mastodon APIs', () => {
)
}
if (input.toString() === 'https://social.com/sven') {
if (request.url === 'https://social.com/sven') {
return new Response(
JSON.stringify({
id: `https://${domain}/ap/users/actor`,
@ -980,13 +978,13 @@ describe('Mastodon APIs', () => {
)
}
if (input.url === 'https://example.com/inbox') {
assert.equal(input.method, 'POST')
receivedActivity = await input.json()
if (request.url === 'https://example.com/inbox') {
assert.equal(request.method, 'POST')
receivedActivity = await request.json()
return new Response('')
}
throw new Error('unexpected request to ' + input)
throw new Error('unexpected request to ' + request.url)
}
})
@ -1005,7 +1003,11 @@ describe('Mastodon APIs', () => {
assert(receivedActivity)
assert.equal(receivedActivity.type, 'Follow')
const row = await db
const row: {
target_actor_acct: string
target_actor_id: string
state: string
} = await db
.prepare(`SELECT target_actor_acct, target_actor_id, state FROM actor_following WHERE actor_id=?`)
.bind(actor.id.toString())
.first()
@ -1036,7 +1038,7 @@ describe('Mastodon APIs', () => {
const row = await db
.prepare(`SELECT count(*) as count FROM actor_following WHERE actor_id=?`)
.bind(actor.id.toString())
.first()
.first<{ count: number }>()
assert(row)
assert.equal(row.count, 0)
})

Wyświetl plik

@ -12,8 +12,9 @@ const domain = 'cloudflare.com'
describe('Mastodon APIs', () => {
describe('media', () => {
test('upload image creates object', async () => {
globalThis.fetch = async (input: any) => {
if (input.url.toString() === 'https://api.cloudflare.com/client/v4/accounts/testaccountid/images/v1') {
globalThis.fetch = async (input: RequestInfo) => {
const request = new Request(input)
if (request.url.toString() === 'https://api.cloudflare.com/client/v4/accounts/testaccountid/images/v1') {
return new Response(
JSON.stringify({
success: true,
@ -24,7 +25,7 @@ describe('Mastodon APIs', () => {
})
)
}
throw new Error('unexpected request to ' + input.url)
throw new Error('unexpected request to ' + request.url)
}
const db = await makeDB()

Wyświetl plik

@ -117,7 +117,7 @@ describe('Mastodon APIs', () => {
globalThis.fetch = async (input: RequestInfo, data: any) => {
if (input === 'https://push.com') {
assert(data.headers['Authorization'].includes('WebPush'))
assert((data.headers['Authorization'] as string).includes('WebPush'))
const cryptoKeyHeader = parseCryptoKey(data.headers['Crypto-Key'])
assert(cryptoKeyHeader.dh)

Wyświetl plik

@ -5,6 +5,7 @@ import * as oauth_token from 'wildebeest/functions/oauth/token'
import { isUrlValid, makeDB, assertCORS, assertJSON, createTestClient } from '../utils'
import { TEST_JWT, ACCESS_CERTS } from '../test-data'
import { strict as assert } from 'node:assert/strict'
import { Actor } from 'wildebeest/backend/src/activitypub/actors'
const userKEK = 'test_kek3'
const accessDomain = 'access.com'
@ -100,7 +101,7 @@ describe('Mastodon APIs', () => {
)
// actor isn't created yet
const { count } = await db.prepare('SELECT count(*) as count FROM actors').first()
const { count } = await db.prepare('SELECT count(*) as count FROM actors').first<{ count: number }>()
assert.equal(count, 0)
})
@ -126,7 +127,9 @@ describe('Mastodon APIs', () => {
const location = res.headers.get('location')
assert.equal(location, 'https://redirect.com/a')
const actor = await db.prepare('SELECT * FROM actors').first()
const actor = await db
.prepare('SELECT * FROM actors')
.first<{ properties: string; email: string; id: string } & Actor>()
const properties = JSON.parse(actor.properties)
assert.equal(actor.email, 'a@cloudflare.com')
@ -134,7 +137,7 @@ describe('Mastodon APIs', () => {
assert.equal(properties.name, 'name')
assert(isUrlValid(actor.id))
// ensure that we generate a correct key pairs for the user
assert((await getSigningKey(userKEK, db, actor)) instanceof CryptoKey)
assert((await getSigningKey(userKEK, db, actor as Actor)) instanceof CryptoKey)
})
test('token error on unknown client', async () => {

Wyświetl plik

@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert/strict'
import { insertReply } from 'wildebeest/backend/src/mastodon/reply'
import { getMentions } from 'wildebeest/backend/src/mastodon/status'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { createPublicNote, Note } from 'wildebeest/backend/src/activitypub/objects/note'
import { createImage } from 'wildebeest/backend/src/activitypub/objects/image'
import * as statuses from 'wildebeest/functions/api/v1/statuses'
import * as statuses_get from 'wildebeest/functions/api/v1/statuses/[id]'
@ -16,6 +16,7 @@ import { isUrlValid, makeDB, assertJSON, streamToArrayBuffer, makeQueue, makeCac
import * as activities from 'wildebeest/backend/src/activitypub/activities'
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
import { MessageType } from 'wildebeest/backend/src/types/queue'
import { MastodonStatus } from 'wildebeest/backend/src/types'
const userKEK = 'test_kek4'
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@ -60,9 +61,9 @@ describe('Mastodon APIs', () => {
assert.equal(res.status, 200)
assertJSON(res)
const data = await res.json<any>()
assert(data.uri.includes('example.com'))
assert(data.uri.includes(data.id))
const data = await res.json<MastodonStatus>()
assert((data.uri as unknown as string).includes('example.com'))
assert((data.uri as unknown as string).includes(data.id))
// Required fields from https://github.com/mastodon/mastodon-android/blob/master/mastodon/src/main/java/org/joinmastodon/android/model/Status.java
assert(data.created_at !== undefined)
assert(data.account !== undefined)
@ -84,7 +85,7 @@ describe('Mastodon APIs', () => {
FROM objects
`
)
.first()
.first<{ content: string; original_actor_id: URL; original_object_id: unknown }>()
assert.equal(row.content, 'my status <p>evil</p>') // note the sanitization
assert.equal(row.original_actor_id.toString(), actor.id.toString())
assert.equal(row.original_object_id, null)
@ -138,7 +139,7 @@ describe('Mastodon APIs', () => {
const res = await statuses.handleRequest(req, db, connectedActor, userKEK, queue, cache)
assert.equal(res.status, 200)
const row = await db.prepare(`SELECT count(*) as count FROM outbox_objects`).first()
const row = await db.prepare(`SELECT count(*) as count FROM outbox_objects`).first<{ count: number }>()
assert.equal(row.count, 1)
})
@ -180,7 +181,7 @@ describe('Mastodon APIs', () => {
})
test('create new status with mention delivers ActivityPub Note', async () => {
let deliveredNote: any = null
let deliveredNote: Note | null = null
globalThis.fetch = async (input: RequestInfo, data: any) => {
if (input.toString() === 'https://remote.com/.well-known/webfinger?resource=acct%3Asven%40remote.com') {
@ -246,12 +247,15 @@ describe('Mastodon APIs', () => {
assert.equal(res.status, 200)
assert(deliveredNote)
assert.equal(deliveredNote.type, 'Create')
assert.equal(deliveredNote.actor, `https://${domain}/ap/users/sven`)
assert.equal(deliveredNote.object.attributedTo, `https://${domain}/ap/users/sven`)
assert.equal(deliveredNote.object.type, 'Note')
assert(deliveredNote.object.to.includes(activities.PUBLIC_GROUP))
assert.equal(deliveredNote.object.cc.length, 1)
assert.equal((deliveredNote as { type: string }).type, 'Create')
assert.equal((deliveredNote as { actor: string }).actor, `https://${domain}/ap/users/sven`)
assert.equal(
(deliveredNote as { object: { attributedTo: string } }).object.attributedTo,
`https://${domain}/ap/users/sven`
)
assert.equal((deliveredNote as { object: { type: string } }).object.type, 'Note')
assert((deliveredNote as { object: { to: string[] } }).object.to.includes(activities.PUBLIC_GROUP))
assert.equal((deliveredNote as { object: { cc: string[] } }).object.cc.length, 1)
})
test('create new status with image', async () => {
@ -302,15 +306,16 @@ describe('Mastodon APIs', () => {
)
.run()
globalThis.fetch = async (input: any) => {
if (input.url === actor.id.toString() + '/inbox') {
assert.equal(input.method, 'POST')
const body = await input.json()
globalThis.fetch = async (input: RequestInfo) => {
const request = new Request(input)
if (request.url === actor.id.toString() + '/inbox') {
assert.equal(request.method, 'POST')
const body = await request.json()
deliveredActivity = body
return new Response()
}
throw new Error('unexpected request to ' + JSON.stringify(input))
throw new Error('unexpected request to ' + request.url)
}
const connectedActor: any = actor
@ -336,7 +341,7 @@ describe('Mastodon APIs', () => {
const data = await res.json<any>()
assert.equal(data.favourited, true)
const row = await db.prepare(`SELECT * FROM actor_favourites`).first()
const row = await db.prepare(`SELECT * FROM actor_favourites`).first<{ actor_id: string; object_id: string }>()
assert.equal(row.actor_id, actor.id.toString())
assert.equal(row.object_id, note.id.toString())
})
@ -469,7 +474,7 @@ describe('Mastodon APIs', () => {
const data = await res.json<any>()
assert.equal(data.reblogged, true)
const row = await db.prepare(`SELECT * FROM actor_reblogs`).first()
const row = await db.prepare(`SELECT * FROM actor_reblogs`).first<{ actor_id: string; object_id: string }>()
assert.equal(row.actor_id, actor.id.toString())
assert.equal(row.object_id, note.id.toString())
})
@ -486,7 +491,7 @@ describe('Mastodon APIs', () => {
const res = await statuses_reblog.handleRequest(db, note.mastodonId!, connectedActor, userKEK, queue, domain)
assert.equal(res.status, 200)
const row = await db.prepare(`SELECT * FROM outbox_objects`).first()
const row = await db.prepare(`SELECT * FROM outbox_objects`).first<{ actor_id: string; object_id: string }>()
assert.equal(row.actor_id, actor.id.toString())
assert.equal(row.object_id, note.id.toString())
})
@ -513,15 +518,16 @@ describe('Mastodon APIs', () => {
)
.run()
globalThis.fetch = async (input: any) => {
if (input.url === 'https://cloudflare.com/ap/users/sven/inbox') {
assert.equal(input.method, 'POST')
const body = await input.json()
globalThis.fetch = async (input: RequestInfo) => {
const request = new Request(input)
if (request.url === 'https://cloudflare.com/ap/users/sven/inbox') {
assert.equal(request.method, 'POST')
const body = await request.json()
deliveredActivity = body
return new Response()
}
throw new Error('unexpected request to ' + JSON.stringify(input))
throw new Error('unexpected request to ' + request.url)
}
const connectedActor: any = actor
@ -588,13 +594,16 @@ describe('Mastodon APIs', () => {
`
)
.bind(data.id)
.first()
.first<{ inReplyTo: string }>()
assert(row !== undefined)
assert.equal(row.inReplyTo, note.id.toString())
}
{
const row = await db.prepare('select * from actor_replies').first()
const row = await db.prepare('select * from actor_replies').first<{
actor_id: string
in_reply_to_object_id: string
}>()
assert(row !== undefined)
assert.equal(row.actor_id, actor.id.toString())
assert.equal(row.in_reply_to_object_id, note.id.toString())
@ -619,7 +628,7 @@ describe('Mastodon APIs', () => {
const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache)
assert.equal(res.status, 400)
const data = await res.json<any>()
const data = await res.json<{ error: string }>()
assert(data.error.includes('Limit exceeded'))
})
@ -644,7 +653,7 @@ describe('Mastodon APIs', () => {
const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache)
assert.equal(res.status, 400)
const data = await res.json<any>()
const data = await res.json<{ error: string }>()
assert(data.error.includes('Limit exceeded'))
})
})

Wyświetl plik

@ -18,7 +18,7 @@ export function isUrlValid(s: string) {
return url.protocol === 'https:'
}
export async function makeDB(): Promise<any> {
export async function makeDB(): Promise<D1Database> {
const db = new Database(':memory:')
const db2 = new BetaDatabase(db)!
@ -27,10 +27,10 @@ export async function makeDB(): Promise<any> {
for (let i = 0, len = migrations.length; i < len; i++) {
const content = await fs.readFile(path.join('migrations', migrations[i]), 'utf-8')
await db.exec(content)
db.exec(content)
}
return db2
return db2 as unknown as D1Database
}
export function assertCORS(response: Response) {

Wyświetl plik

@ -1,7 +1,7 @@
import { ContextData } from 'wildebeest/backend/src/types/context'
import { cors } from 'wildebeest/backend/src/utils/cors'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { Env } from 'wildebeest/backend/src/types/env'
import type { Env } from 'wildebeest/backend/src/types/env'
import { createClient } from 'wildebeest/backend/src/mastodon/client'
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'

Wyświetl plik

@ -1,6 +1,6 @@
// https://docs.joinmastodon.org/methods/notifications/#get-one
import type { Notification } from 'wildebeest/backend/src/types/notification'
import type { Notification, NotificationsQueryResult } from 'wildebeest/backend/src/types/notification'
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
import { getPersonById } from 'wildebeest/backend/src/activitypub/actors'
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
@ -36,7 +36,7 @@ export async function handleRequest(
WHERE actor_notifications.id=? AND actor_notifications.actor_id=?
`
const row: any = await db.prepare(query).bind(id, connectedActor.id.toString()).first()
const row = await db.prepare(query).bind(id, connectedActor.id.toString()).first<NotificationsQueryResult>()
const from_actor_id = new URL(row.from_actor_id)
const fromActor = await getPersonById(db, from_actor_id)

Wyświetl plik

@ -6,7 +6,7 @@ import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
import { createSubscription, getSubscription } from 'wildebeest/backend/src/mastodon/subscription'
import type { CreateRequest } from 'wildebeest/backend/src/mastodon/subscription'
import { ContextData } from 'wildebeest/backend/src/types/context'
import { Env } from 'wildebeest/backend/src/types/env'
import type { Env } from 'wildebeest/backend/src/types/env'
import * as errors from 'wildebeest/backend/src/errors'
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'