kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #176 from cloudflare/sven/reply-uses-original-object
move internal Object fields to symbolspull/177/head
commit
48ab322250
|
@ -24,6 +24,7 @@ import { insertLike } from 'wildebeest/backend/src/mastodon/like'
|
|||
import { createReblog } from 'wildebeest/backend/src/mastodon/reblog'
|
||||
import { insertReply } from 'wildebeest/backend/src/mastodon/reply'
|
||||
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
|
||||
import { originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
function extractID(domain: string, s: string | URL): string {
|
||||
return s.toString().replace(`https://${domain}/ap/users/`, '')
|
||||
|
@ -112,7 +113,7 @@ export async function handle(
|
|||
throw new Error(`object ${objectId} does not exist`)
|
||||
}
|
||||
|
||||
if (actorId.toString() !== object.originalActorId) {
|
||||
if (actorId.toString() !== object[originalActorIdSymbol]) {
|
||||
throw new Error('actorid mismatch when updating object')
|
||||
}
|
||||
|
||||
|
@ -278,7 +279,7 @@ export async function handle(
|
|||
const fromActor = await actors.getAndCache(actorId, db)
|
||||
|
||||
// notify the user
|
||||
const targetActor = await actors.getPersonById(db, new URL(obj.originalActorId))
|
||||
const targetActor = await actors.getPersonById(db, new URL(obj[originalActorIdSymbol]))
|
||||
if (targetActor === null) {
|
||||
console.warn('object actor not found')
|
||||
break
|
||||
|
@ -300,13 +301,13 @@ export async function handle(
|
|||
const objectId = getObjectAsId()
|
||||
|
||||
const obj = await objects.getObjectById(db, objectId)
|
||||
if (obj === null || !obj.originalActorId) {
|
||||
if (obj === null || !obj[originalActorIdSymbol]) {
|
||||
console.warn('unknown object')
|
||||
break
|
||||
}
|
||||
|
||||
const fromActor = await actors.getAndCache(actorId, db)
|
||||
const targetActor = await actors.getPersonById(db, new URL(obj.originalActorId))
|
||||
const targetActor = await actors.getPersonById(db, new URL(obj[originalActorIdSymbol]))
|
||||
if (targetActor === null) {
|
||||
console.warn('object actor not found')
|
||||
break
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import type { UUID } from 'wildebeest/backend/src/types'
|
||||
|
||||
export const originalActorIdSymbol = Symbol()
|
||||
export const originalObjectIdSymbol = Symbol()
|
||||
export const mastodonIdSymbol = Symbol()
|
||||
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
export interface APObject {
|
||||
type: string
|
||||
|
@ -19,9 +23,9 @@ export interface APObject {
|
|||
// Extension
|
||||
preferredUsername?: string
|
||||
// Internal
|
||||
originalActorId?: string
|
||||
originalObjectId?: string
|
||||
mastodonId?: UUID
|
||||
[originalActorIdSymbol]?: string
|
||||
[originalObjectIdSymbol]?: string
|
||||
[mastodonIdSymbol]?: UUID
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
|
||||
|
@ -54,9 +58,10 @@ export async function createObject<Type extends APObject>(
|
|||
...sanitizedProperties,
|
||||
type,
|
||||
id: new URL(row.id),
|
||||
mastodonId: row.mastodon_id,
|
||||
published: new Date(row.cdate).toISOString(),
|
||||
originalActorId: row.original_actor_id,
|
||||
|
||||
[mastodonIdSymbol]: row.mastodon_id,
|
||||
[originalActorIdSymbol]: row.original_actor_id,
|
||||
} as Type
|
||||
}
|
||||
|
||||
|
@ -121,9 +126,10 @@ export async function cacheObject(
|
|||
|
||||
type: row.type,
|
||||
id: new URL(row.id),
|
||||
mastodonId: row.mastodon_id,
|
||||
originalActorId: row.original_actor_id,
|
||||
originalObjectId: row.original_object_id,
|
||||
|
||||
[mastodonIdSymbol]: row.mastodon_id,
|
||||
[originalActorIdSymbol]: row.original_actor_id,
|
||||
[originalObjectIdSymbol]: row.original_object_id,
|
||||
} as APObject
|
||||
|
||||
return { object, created: true }
|
||||
|
@ -178,9 +184,10 @@ WHERE objects.${key}=?
|
|||
|
||||
type: result.type,
|
||||
id: new URL(result.id),
|
||||
mastodonId: result.mastodon_id,
|
||||
originalActorId: result.original_actor_id,
|
||||
originalObjectId: result.original_object_id,
|
||||
|
||||
[mastodonIdSymbol]: result.mastodon_id,
|
||||
[originalActorIdSymbol]: result.original_actor_id,
|
||||
[originalObjectIdSymbol]: result.original_object_id,
|
||||
} as APObject
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { queryAcct } from 'wildebeest/backend/src/webfinger'
|
||||
import type { MediaAttachment } from 'wildebeest/backend/src/types/media'
|
||||
import type { UUID } from 'wildebeest/backend/src/types'
|
||||
import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects'
|
||||
import {
|
||||
getObjectByMastodonId,
|
||||
mastodonIdSymbol,
|
||||
originalActorIdSymbol,
|
||||
} from 'wildebeest/backend/src/activitypub/objects'
|
||||
import { createPublicNote, type Note } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
||||
|
@ -46,12 +50,12 @@ export async function toMastodonStatusFromObject(
|
|||
obj: Note,
|
||||
domain: string
|
||||
): Promise<MastodonStatus | null> {
|
||||
if (obj.originalActorId === undefined) {
|
||||
if (obj[originalActorIdSymbol] === undefined) {
|
||||
console.warn('missing `obj.originalActorId`')
|
||||
return null
|
||||
}
|
||||
|
||||
const actorId = new URL(obj.originalActorId)
|
||||
const actorId = new URL(obj[originalActorIdSymbol])
|
||||
const actor = await actors.getAndCache(actorId, db)
|
||||
|
||||
const acct = urlToHandle(actorId)
|
||||
|
@ -81,9 +85,9 @@ export async function toMastodonStatusFromObject(
|
|||
|
||||
media_attachments: mediaAttachments,
|
||||
content: obj.content || '',
|
||||
id: obj.mastodonId || '',
|
||||
id: obj[mastodonIdSymbol] || '',
|
||||
uri: obj.id,
|
||||
url: new URL('/statuses/' + obj.mastodonId, 'https://' + domain),
|
||||
url: new URL('/statuses/' + obj[mastodonIdSymbol], 'https://' + domain),
|
||||
created_at: obj.published || '',
|
||||
account,
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { MediaAttachment } from 'wildebeest/backend/src/types/media'
|
|||
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
|
||||
import { IMAGE } from 'wildebeest/backend/src/activitypub/objects/image'
|
||||
import type { APObject } from 'wildebeest/backend/src/activitypub/objects'
|
||||
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
export function fromObject(obj: APObject): MediaAttachment {
|
||||
if (obj.type === IMAGE) {
|
||||
|
@ -28,7 +29,7 @@ export function fromObjectDocument(obj: Document): MediaAttachment {
|
|||
function fromObjectImage(obj: APObject): MediaAttachment {
|
||||
return {
|
||||
url: new URL(obj.url),
|
||||
id: obj.mastodonId || obj.url.toString(),
|
||||
id: obj[mastodonIdSymbol] || obj.url.toString(),
|
||||
preview_url: new URL(obj.url),
|
||||
type: 'image',
|
||||
meta: {
|
||||
|
|
|
@ -12,6 +12,7 @@ import * as ap_outbox from 'wildebeest/functions/ap/users/[id]/outbox'
|
|||
import * as ap_inbox from 'wildebeest/functions/ap/users/[id]/inbox'
|
||||
import * as ap_outbox_page from 'wildebeest/functions/ap/users/[id]/outbox/page'
|
||||
import { createStatus } from '../src/mastodon/status'
|
||||
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
const userKEK = 'test_kek5'
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
|
@ -169,7 +170,7 @@ describe('ActivityPub', () => {
|
|||
const actor = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||
const note = await createPublicNote(domain, db, 'content', actor)
|
||||
|
||||
const res = await ap_objects.handleRequest(domain, db, note.mastodonId!)
|
||||
const res = await ap_objects.handleRequest(domain, db, note[mastodonIdSymbol]!)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
|
|
@ -7,6 +7,7 @@ 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'
|
||||
import { originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
const adminEmail = 'admin@example.com'
|
||||
const domain = 'cloudflare.com'
|
||||
|
@ -337,11 +338,11 @@ describe('ActivityPub', () => {
|
|||
|
||||
const obj: any = await getObjectById(db, entry.object_id)
|
||||
assert(obj)
|
||||
assert.equal(obj.originalObjectId, 'https://example.com/note2')
|
||||
assert.equal(obj[originalObjectIdSymbol], 'https://example.com/note2')
|
||||
|
||||
const inReplyTo: any = await getObjectById(db, entry.in_reply_to_object_id)
|
||||
assert(inReplyTo)
|
||||
assert.equal(inReplyTo.originalObjectId, 'https://example.com/note1')
|
||||
assert.equal(inReplyTo[originalObjectIdSymbol], 'https://example.com/note1')
|
||||
})
|
||||
|
||||
test('preserve Note sent with `to`', async () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
|||
import { strict as assert } from 'node:assert/strict'
|
||||
import { makeDB, assertJSON, isUrlValid } from '../utils'
|
||||
import * as objects from 'wildebeest/backend/src/activitypub/objects'
|
||||
import { mastodonIdSymbol, originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
const userKEK = 'test_kek10'
|
||||
const CF_ACCOUNT_ID = 'testaccountid'
|
||||
|
@ -51,9 +52,9 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const obj = await objects.getObjectByMastodonId(db, data.id)
|
||||
assert(obj)
|
||||
assert(obj.mastodonId)
|
||||
assert(obj[mastodonIdSymbol])
|
||||
assert.equal(obj.type, 'Image')
|
||||
assert.equal(obj.originalActorId, connectedActor.id.toString())
|
||||
assert.equal(obj[originalActorIdSymbol], connectedActor.id.toString())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,6 +10,7 @@ import { sendLikeNotification } from 'wildebeest/backend/src/mastodon/notificati
|
|||
import { createSubscription } from 'wildebeest/backend/src/mastodon/subscription'
|
||||
import { arrayBufferToBase64 } from 'wildebeest/backend/src/utils/key-ops'
|
||||
import { getNotifications } from 'wildebeest/backend/src/mastodon/notification'
|
||||
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
const userKEK = 'test_kek15'
|
||||
const domain = 'cloudflare.com'
|
||||
|
@ -58,11 +59,11 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
assert.equal(notifications[0].type, 'mention')
|
||||
assert.equal(notifications[0].account.username, 'from')
|
||||
assert.equal(notifications[0].status.id, note.mastodonId)
|
||||
assert.equal(notifications[0].status.id, note[mastodonIdSymbol])
|
||||
|
||||
assert.equal(notifications[1].type, 'favourite')
|
||||
assert.equal(notifications[1].account.username, 'from')
|
||||
assert.equal(notifications[1].status.id, note.mastodonId)
|
||||
assert.equal(notifications[1].status.id, note[mastodonIdSymbol])
|
||||
assert.equal(notifications[1].status.account.username, 'sven')
|
||||
|
||||
assert.equal(notifications[2].type, 'follow')
|
||||
|
|
|
@ -17,6 +17,7 @@ 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'
|
||||
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
const userKEK = 'test_kek4'
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
|
@ -325,7 +326,7 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const body = {
|
||||
status: 'my status',
|
||||
media_ids: [image.mastodonId],
|
||||
media_ids: [image[mastodonIdSymbol]],
|
||||
visibility: 'public',
|
||||
}
|
||||
const req = new Request('https://example.com', {
|
||||
|
@ -392,7 +393,7 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const connectedActor: any = actor
|
||||
|
||||
const res = await statuses_favourite.handleRequest(db, note.mastodonId!, connectedActor, userKEK, domain)
|
||||
const res = await statuses_favourite.handleRequest(db, note[mastodonIdSymbol]!, connectedActor, userKEK, domain)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -536,7 +537,7 @@ describe('Mastodon APIs', () => {
|
|||
await insertLike(db, actor2, note)
|
||||
await insertLike(db, actor3, note)
|
||||
|
||||
const res = await statuses_get.handleRequest(db, note.mastodonId!, domain)
|
||||
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -552,7 +553,7 @@ describe('Mastodon APIs', () => {
|
|||
const mediaAttachments = [await createImage(domain, db, actor, properties)]
|
||||
const note = await createPublicNote(domain, db, 'my first status', actor, mediaAttachments)
|
||||
|
||||
const res = await statuses_get.handleRequest(db, note.mastodonId!, domain)
|
||||
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -571,7 +572,7 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
await createReply(domain, db, actor, note, 'a reply')
|
||||
|
||||
const res = await statuses_context.handleRequest(domain, db, note.mastodonId!)
|
||||
const res = await statuses_context.handleRequest(domain, db, note[mastodonIdSymbol]!)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -591,7 +592,7 @@ describe('Mastodon APIs', () => {
|
|||
await insertReblog(db, actor2, note)
|
||||
await insertReblog(db, actor3, note)
|
||||
|
||||
const res = await statuses_get.handleRequest(db, note.mastodonId!, domain)
|
||||
const res = await statuses_get.handleRequest(db, note[mastodonIdSymbol]!, domain)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -607,7 +608,14 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const connectedActor: any = actor
|
||||
|
||||
const res = await statuses_reblog.handleRequest(db, note.mastodonId!, connectedActor, userKEK, queue, domain)
|
||||
const res = await statuses_reblog.handleRequest(
|
||||
db,
|
||||
note[mastodonIdSymbol]!,
|
||||
connectedActor,
|
||||
userKEK,
|
||||
queue,
|
||||
domain
|
||||
)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const data = await res.json<any>()
|
||||
|
@ -627,7 +635,14 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const connectedActor: any = actor
|
||||
|
||||
const res = await statuses_reblog.handleRequest(db, note.mastodonId!, connectedActor, userKEK, queue, domain)
|
||||
const res = await statuses_reblog.handleRequest(
|
||||
db,
|
||||
note[mastodonIdSymbol]!,
|
||||
connectedActor,
|
||||
userKEK,
|
||||
queue,
|
||||
domain
|
||||
)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const row = await db.prepare(`SELECT * FROM outbox_objects`).first<{ actor_id: string; object_id: string }>()
|
||||
|
@ -709,7 +724,7 @@ describe('Mastodon APIs', () => {
|
|||
|
||||
const body = {
|
||||
status: 'my reply',
|
||||
in_reply_to_id: note.mastodonId,
|
||||
in_reply_to_id: note[mastodonIdSymbol],
|
||||
visibility: 'public',
|
||||
}
|
||||
const req = new Request('https://example.com', {
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { Cache } from 'wildebeest/backend/src/cache'
|
|||
import { cacheFromEnv } from 'wildebeest/backend/src/cache'
|
||||
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
|
||||
import { newMention } from 'wildebeest/backend/src/activitypub/objects/mention'
|
||||
import { originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
type StatusCreate = {
|
||||
status: string
|
||||
|
@ -84,7 +85,7 @@ export async function handleRequest(
|
|||
|
||||
const extraProperties: any = {}
|
||||
if (inReplyToObject !== null) {
|
||||
extraProperties.inReplyTo = inReplyToObject.originalObjectId || inReplyToObject.id.toString()
|
||||
extraProperties.inReplyTo = inReplyToObject[originalObjectIdSymbol] || inReplyToObject.id.toString()
|
||||
}
|
||||
|
||||
const domain = new URL(request.url).hostname
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/object
|
|||
import type { Note } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
|
||||
import { originalObjectIdSymbol, originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ env, data, params, request }) => {
|
||||
const domain = new URL(request.url).hostname
|
||||
|
@ -35,14 +36,14 @@ export async function handleRequest(
|
|||
return new Response('', { status: 404 })
|
||||
}
|
||||
|
||||
if (obj.originalObjectId && obj.originalActorId) {
|
||||
if (obj[originalObjectIdSymbol] && obj[originalActorIdSymbol]) {
|
||||
// Liking an external object delivers the like activity
|
||||
const targetActor = await actors.getAndCache(new URL(obj.originalActorId), db)
|
||||
const targetActor = await actors.getAndCache(new URL(obj[originalActorIdSymbol]), db)
|
||||
if (!targetActor) {
|
||||
return new Response(`target Actor ${obj.originalActorId} not found`, { status: 404 })
|
||||
return new Response(`target Actor ${obj[originalActorIdSymbol]} not found`, { status: 404 })
|
||||
}
|
||||
|
||||
const activity = like.create(connectedActor, new URL(obj.originalObjectId))
|
||||
const activity = like.create(connectedActor, new URL(obj[originalObjectIdSymbol]))
|
||||
const signingKey = await getSigningKey(userKEK, db, connectedActor)
|
||||
await deliverToActor(signingKey, connectedActor, targetActor, activity)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/object
|
|||
import type { Note } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import { toMastodonStatusFromObject } from 'wildebeest/backend/src/mastodon/status'
|
||||
import { originalActorIdSymbol, originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ env, data, params, request }) => {
|
||||
const domain = new URL(request.url).hostname
|
||||
|
@ -36,15 +37,15 @@ export async function handleRequest(
|
|||
return new Response('', { status: 404 })
|
||||
}
|
||||
|
||||
if (obj.originalObjectId && obj.originalActorId) {
|
||||
if (obj[originalObjectIdSymbol] && obj[originalActorIdSymbol]) {
|
||||
// Rebloggin an external object delivers the announce activity to the
|
||||
// post author.
|
||||
const targetActor = await actors.getAndCache(new URL(obj.originalActorId), db)
|
||||
const targetActor = await actors.getAndCache(new URL(obj[originalActorIdSymbol]), db)
|
||||
if (!targetActor) {
|
||||
return new Response(`target Actor ${obj.originalActorId} not found`, { status: 404 })
|
||||
return new Response(`target Actor ${obj[originalActorIdSymbol]} not found`, { status: 404 })
|
||||
}
|
||||
|
||||
const activity = announce.create(connectedActor, new URL(obj.originalObjectId))
|
||||
const activity = announce.create(connectedActor, new URL(obj[originalObjectIdSymbol]))
|
||||
const signingKey = await getSigningKey(userKEK, db, connectedActor)
|
||||
|
||||
await Promise.all([
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as media from 'wildebeest/backend/src/media/image'
|
|||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import type { MediaAttachment } from 'wildebeest/backend/src/types/media'
|
||||
import type { Person } from 'wildebeest/backend/src/activitypub/actors'
|
||||
import { mastodonIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env, data }) => {
|
||||
return handleRequestPost(request, env.DATABASE, data.connectedActor, env.CF_ACCOUNT_ID, env.CF_API_TOKEN)
|
||||
|
@ -34,7 +35,7 @@ export async function handleRequestPost(
|
|||
console.log({ image })
|
||||
|
||||
const res: MediaAttachment = {
|
||||
id: image.mastodonId!,
|
||||
id: image[mastodonIdSymbol]!,
|
||||
url: image.url,
|
||||
preview_url: image.url,
|
||||
type: 'image',
|
||||
|
|
Ładowanie…
Reference in New Issue