diff --git a/a.txt b/a.txt deleted file mode 100644 index f2d45af..0000000 --- a/a.txt +++ /dev/null @@ -1,4125 +0,0 @@ -./pkg/README.md -``` -# TODO -``` -./pkg/package.json -``` -{ - "name": "pubky-app-specs", - "type": "module", - "description": "Pubky.app Data Model Specifications", - "version": "0.3.0", - "license": "MIT", - "collaborators": [ - "SHAcollision" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/pubky/pubky-app-specs.git" - }, - "files": [ - "pubky_app_specs_bg.wasm", - "pubky_app_specs.js", - "pubky_app_specs.d.ts" - ], - "main": "pubky_app_specs.js", - "homepage": "https://pubky.app", - "types": "pubky_app_specs.d.ts", - "sideEffects": [ - "./snippets/*" - ] -} -``` -./dist/pubky_app_specs_bg.wasm.d.ts -``` -/* tslint:disable */ -/* eslint-disable */ -export const memory: WebAssembly.Memory; -export const __wbg_pubkyappuser_free: (a: number, b: number) => void; -export const __wbg_pubkyappuserlink_free: (a: number, b: number) => void; -export const pubkyappuser_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => number; -export const pubkyappuser_get_data: (a: number) => [number, number, number]; -export const pubkyappuserlink_new: (a: number, b: number, c: number, d: number) => number; -export const __wbg_pubkyappspecs_free: (a: number, b: number) => void; -export const pubkyappspecs_new: (a: number, b: number) => number; -export const pubkyappspecs_createFollow: (a: number, b: number, c: number) => [number, number, number]; -export const create_pubky_app_user: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: number, i: number) => [number, number, number]; -export const create_pubky_app_feed: (a: any, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number]; -export const create_pubky_app_file: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => [number, number, number]; -export const create_pubky_app_post: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: any) => [number, number, number]; -export const create_pubky_app_tag: (a: number, b: number, c: number, d: number) => [number, number, number]; -export const create_pubky_app_bookmark: (a: number, b: number) => [number, number, number]; -export const create_pubky_app_follow: (a: number, b: number) => [number, number, number]; -export const create_pubky_app_mute: (a: number, b: number) => [number, number, number]; -export const create_pubky_app_last_read: () => [number, number, number]; -export const __wbindgen_malloc: (a: number, b: number) => number; -export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; -export const __wbindgen_exn_store: (a: number) => void; -export const __externref_table_alloc: () => number; -export const __wbindgen_export_4: WebAssembly.Table; -export const __externref_table_dealloc: (a: number) => void; -export const __wbindgen_start: () => void; -``` -./dist/pubky_app_specs.js -``` -let wasm; - -let WASM_VECTOR_LEN = 0; - -let cachedUint8ArrayMemory0 = null; - -function getUint8ArrayMemory0() { - if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { - cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8ArrayMemory0; -} - -const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); - -const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' - ? function (arg, view) { - return cachedTextEncoder.encodeInto(arg, view); -} - : function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; -}); - -function passStringToWasm0(arg, malloc, realloc) { - - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - - const mem = getUint8ArrayMemory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; - const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - const ret = encodeString(arg, view); - - offset += ret.written; - ptr = realloc(ptr, len, offset, 1) >>> 0; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -let cachedDataViewMemory0 = null; - -function getDataViewMemory0() { - if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { - cachedDataViewMemory0 = new DataView(wasm.memory.buffer); - } - return cachedDataViewMemory0; -} - -function addToExternrefTable0(obj) { - const idx = wasm.__externref_table_alloc(); - wasm.__wbindgen_export_4.set(idx, obj); - return idx; -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - const idx = addToExternrefTable0(e); - wasm.__wbindgen_exn_store(idx); - } -} - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches && builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); - -if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -function passArrayJsValueToWasm0(array, malloc) { - const ptr = malloc(array.length * 4, 4) >>> 0; - for (let i = 0; i < array.length; i++) { - const add = addToExternrefTable0(array[i]); - getDataViewMemory0().setUint32(ptr + 4 * i, add, true); - } - WASM_VECTOR_LEN = array.length; - return ptr; -} - -function takeFromExternrefTable0(idx) { - const value = wasm.__wbindgen_export_4.get(idx); - wasm.__externref_table_dealloc(idx); - return value; -} -/** - * @param {string} name - * @param {string | null | undefined} bio - * @param {string | null | undefined} image - * @param {any} links - * @param {string | null} [status] - * @returns {any} - */ -export function create_pubky_app_user(name, bio, image, links, status) { - const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - var ptr1 = isLikeNone(bio) ? 0 : passStringToWasm0(bio, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - var ptr2 = isLikeNone(image) ? 0 : passStringToWasm0(image, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len2 = WASM_VECTOR_LEN; - var ptr3 = isLikeNone(status) ? 0 : passStringToWasm0(status, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len3 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_user(ptr0, len0, ptr1, len1, ptr2, len2, links, ptr3, len3); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {any} tags - * @param {string} reach - * @param {string} layout - * @param {string} sort - * @param {string | null | undefined} content - * @param {string} name - * @returns {any} - */ -export function create_pubky_app_feed(tags, reach, layout, sort, content, name) { - const ptr0 = passStringToWasm0(reach, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(layout, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(sort, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len2 = WASM_VECTOR_LEN; - var ptr3 = isLikeNone(content) ? 0 : passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len3 = WASM_VECTOR_LEN; - const ptr4 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len4 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_feed(tags, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} name - * @param {string} src - * @param {string} content_type - * @param {bigint} size - * @returns {any} - */ -export function create_pubky_app_file(name, src, content_type, size) { - const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(content_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len2 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_file(ptr0, len0, ptr1, len1, ptr2, len2, size); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} content - * @param {string} kind - * @param {string | null | undefined} parent - * @param {any} embed - * @param {any} attachments - * @returns {any} - */ -export function create_pubky_app_post(content, kind, parent, embed, attachments) { - const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(kind, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - var ptr2 = isLikeNone(parent) ? 0 : passStringToWasm0(parent, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len2 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_post(ptr0, len0, ptr1, len1, ptr2, len2, embed, attachments); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} uri - * @param {string} label - * @returns {any} - */ -export function create_pubky_app_tag(uri, label) { - const ptr0 = passStringToWasm0(uri, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(label, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_tag(ptr0, len0, ptr1, len1); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} uri - * @returns {any} - */ -export function create_pubky_app_bookmark(uri) { - const ptr0 = passStringToWasm0(uri, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_bookmark(ptr0, len0); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} pubky_id - * @returns {any} - */ -export function create_pubky_app_follow(pubky_id) { - const ptr0 = passStringToWasm0(pubky_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_follow(ptr0, len0); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @param {string} pubky_id - * @returns {any} - */ -export function create_pubky_app_mute(pubky_id) { - const ptr0 = passStringToWasm0(pubky_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.create_pubky_app_mute(ptr0, len0); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -/** - * @returns {any} - */ -export function create_pubky_app_last_read() { - const ret = wasm.create_pubky_app_last_read(); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); -} - -const PubkyAppSpecsFinalization = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_pubkyappspecs_free(ptr >>> 0, 1)); -/** - * Represents a user's single link with a title and URL. - */ -export class PubkyAppSpecs { - - __destroy_into_raw() { - const ptr = this.__wbg_ptr; - this.__wbg_ptr = 0; - PubkyAppSpecsFinalization.unregister(this); - return ptr; - } - - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_pubkyappspecs_free(ptr, 0); - } - /** - * Creates a new `PubkyAppSpecs` instance. - * @param {string} pubky_id - */ - constructor(pubky_id) { - const ptr0 = passStringToWasm0(pubky_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.pubkyappspecs_new(ptr0, len0); - this.__wbg_ptr = ret >>> 0; - PubkyAppSpecsFinalization.register(this, this.__wbg_ptr, this); - return this; - } - /** - * @param {string} followee_id - * @returns {any} - */ - createFollow(followee_id) { - const ptr0 = passStringToWasm0(followee_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.pubkyappspecs_createFollow(this.__wbg_ptr, ptr0, len0); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); - } -} - -const PubkyAppUserFinalization = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_pubkyappuser_free(ptr >>> 0, 1)); -/** - * URI: /pub/pubky.app/profile.json - */ -export class PubkyAppUser { - - __destroy_into_raw() { - const ptr = this.__wbg_ptr; - this.__wbg_ptr = 0; - PubkyAppUserFinalization.unregister(this); - return ptr; - } - - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_pubkyappuser_free(ptr, 0); - } - /** - * Creates a new `PubkyAppUser` instance and sanitizes it. - * @param {string} name - * @param {string | null} [bio] - * @param {string | null} [image] - * @param {PubkyAppUserLink[] | null} [links] - * @param {string | null} [status] - */ - constructor(name, bio, image, links, status) { - const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - var ptr1 = isLikeNone(bio) ? 0 : passStringToWasm0(bio, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - var ptr2 = isLikeNone(image) ? 0 : passStringToWasm0(image, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len2 = WASM_VECTOR_LEN; - var ptr3 = isLikeNone(links) ? 0 : passArrayJsValueToWasm0(links, wasm.__wbindgen_malloc); - var len3 = WASM_VECTOR_LEN; - var ptr4 = isLikeNone(status) ? 0 : passStringToWasm0(status, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len4 = WASM_VECTOR_LEN; - const ret = wasm.pubkyappuser_new(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4); - this.__wbg_ptr = ret >>> 0; - PubkyAppUserFinalization.register(this, this.__wbg_ptr, this); - return this; - } - /** - * @returns {any} - */ - get_data() { - const ret = wasm.pubkyappuser_get_data(this.__wbg_ptr); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); - } -} - -const PubkyAppUserLinkFinalization = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_pubkyappuserlink_free(ptr >>> 0, 1)); -/** - * Represents a user's single link with a title and URL. - */ -export class PubkyAppUserLink { - - static __unwrap(jsValue) { - if (!(jsValue instanceof PubkyAppUserLink)) { - return 0; - } - return jsValue.__destroy_into_raw(); - } - - __destroy_into_raw() { - const ptr = this.__wbg_ptr; - this.__wbg_ptr = 0; - PubkyAppUserLinkFinalization.unregister(this); - return ptr; - } - - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_pubkyappuserlink_free(ptr, 0); - } - /** - * Creates a new `PubkyAppUserLink` instance and sanitizes it. - * @param {string} title - * @param {string} url - */ - constructor(title, url) { - const ptr0 = passStringToWasm0(title, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.pubkyappuserlink_new(ptr0, len0, ptr1, len1); - this.__wbg_ptr = ret >>> 0; - PubkyAppUserLinkFinalization.register(this, this.__wbg_ptr, this); - return this; - } -} - -async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - - } catch (e) { - if (module.headers.get('Content-Type') != 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { - throw e; - } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - - } else { - return instance; - } - } -} - -function __wbg_get_imports() { - const imports = {}; - imports.wbg = {}; - imports.wbg.__wbg_String_8f0eb39a4a4c2f66 = function(arg0, arg1) { - const ret = String(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) { - const ret = arg0.buffer; - return ret; - }; - imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { - const ret = arg0.call(arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_done_769e5ede4b31c67b = function(arg0) { - const ret = arg0.done; - return ret; - }; - imports.wbg.__wbg_entries_3265d4158b33e5dc = function(arg0) { - const ret = Object.entries(arg0); - return ret; - }; - imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(arg0, arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_get_b9b93047fe3cf45b = function(arg0, arg1) { - const ret = arg0[arg1 >>> 0]; - return ret; - }; - imports.wbg.__wbg_getwithrefkey_1dc361bd10053bfe = function(arg0, arg1) { - const ret = arg0[arg1]; - return ret; - }; - imports.wbg.__wbg_instanceof_ArrayBuffer_e14585432e3737fc = function(arg0) { - let result; - try { - result = arg0 instanceof ArrayBuffer; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Uint8Array_17156bcf118086a9 = function(arg0) { - let result; - try { - result = arg0 instanceof Uint8Array; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_isArray_a1eab7e0d067391b = function(arg0) { - const ret = Array.isArray(arg0); - return ret; - }; - imports.wbg.__wbg_iterator_9a24c88df860dc65 = function() { - const ret = Symbol.iterator; - return ret; - }; - imports.wbg.__wbg_length_a446193dc22c12f8 = function(arg0) { - const ret = arg0.length; - return ret; - }; - imports.wbg.__wbg_length_e2d2a49132c1b256 = function(arg0) { - const ret = arg0.length; - return ret; - }; - imports.wbg.__wbg_new_405e22f390576ce2 = function() { - const ret = new Object(); - return ret; - }; - imports.wbg.__wbg_new_78feb108b6472713 = function() { - const ret = new Array(); - return ret; - }; - imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { - const ret = new Uint8Array(arg0); - return ret; - }; - imports.wbg.__wbg_next_25feadfc0913fea9 = function(arg0) { - const ret = arg0.next; - return ret; - }; - imports.wbg.__wbg_next_6574e1a8a62d1055 = function() { return handleError(function (arg0) { - const ret = arg0.next(); - return ret; - }, arguments) }; - imports.wbg.__wbg_now_807e54c39636c349 = function() { - const ret = Date.now(); - return ret; - }; - imports.wbg.__wbg_pubkyappuserlink_unwrap = function(arg0) { - const ret = PubkyAppUserLink.__unwrap(arg0); - return ret; - }; - imports.wbg.__wbg_set_37837023f3d740e8 = function(arg0, arg1, arg2) { - arg0[arg1 >>> 0] = arg2; - }; - imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { - arg0[arg1] = arg2; - }; - imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { - arg0.set(arg1, arg2 >>> 0); - }; - imports.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = Reflect.set(arg0, arg1, arg2); - return ret; - }, arguments) }; - imports.wbg.__wbg_value_cd1ffa7b1ab794f1 = function(arg0) { - const ret = arg0.value; - return ret; - }; - imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) { - const ret = arg0; - return ret; - }; - imports.wbg.__wbindgen_boolean_get = function(arg0) { - const v = arg0; - const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; - return ret; - }; - imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { - const ret = debugString(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbindgen_error_new = function(arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return ret; - }; - imports.wbg.__wbindgen_in = function(arg0, arg1) { - const ret = arg0 in arg1; - return ret; - }; - imports.wbg.__wbindgen_init_externref_table = function() { - const table = wasm.__wbindgen_export_4; - const offset = table.grow(4); - table.set(0, undefined); - table.set(offset + 0, undefined); - table.set(offset + 1, null); - table.set(offset + 2, true); - table.set(offset + 3, false); - ; - }; - imports.wbg.__wbindgen_is_function = function(arg0) { - const ret = typeof(arg0) === 'function'; - return ret; - }; - imports.wbg.__wbindgen_is_null = function(arg0) { - const ret = arg0 === null; - return ret; - }; - imports.wbg.__wbindgen_is_object = function(arg0) { - const val = arg0; - const ret = typeof(val) === 'object' && val !== null; - return ret; - }; - imports.wbg.__wbindgen_is_string = function(arg0) { - const ret = typeof(arg0) === 'string'; - return ret; - }; - imports.wbg.__wbindgen_is_undefined = function(arg0) { - const ret = arg0 === undefined; - return ret; - }; - imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) { - const ret = arg0 == arg1; - return ret; - }; - imports.wbg.__wbindgen_memory = function() { - const ret = wasm.memory; - return ret; - }; - imports.wbg.__wbindgen_number_get = function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'number' ? obj : undefined; - getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }; - imports.wbg.__wbindgen_number_new = function(arg0) { - const ret = arg0; - return ret; - }; - imports.wbg.__wbindgen_string_get = function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - const ret = getStringFromWasm0(arg0, arg1); - return ret; - }; - imports.wbg.__wbindgen_throw = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; - - return imports; -} - -function __wbg_init_memory(imports, memory) { - -} - -function __wbg_finalize_init(instance, module) { - wasm = instance.exports; - __wbg_init.__wbindgen_wasm_module = module; - cachedDataViewMemory0 = null; - cachedUint8ArrayMemory0 = null; - - - wasm.__wbindgen_start(); - return wasm; -} - -function initSync(module) { - if (wasm !== undefined) return wasm; - - - if (typeof module !== 'undefined') { - if (Object.getPrototypeOf(module) === Object.prototype) { - ({module} = module) - } else { - console.warn('using deprecated parameters for `initSync()`; pass a single object instead') - } - } - - const imports = __wbg_get_imports(); - - __wbg_init_memory(imports); - - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - - const instance = new WebAssembly.Instance(module, imports); - - return __wbg_finalize_init(instance, module); -} - -async function __wbg_init(module_or_path) { - if (wasm !== undefined) return wasm; - - - if (typeof module_or_path !== 'undefined') { - if (Object.getPrototypeOf(module_or_path) === Object.prototype) { - ({module_or_path} = module_or_path) - } else { - console.warn('using deprecated parameters for the initialization function; pass a single object instead') - } - } - - if (typeof module_or_path === 'undefined') { - module_or_path = new URL('pubky_app_specs_bg.wasm', import.meta.url); - } - const imports = __wbg_get_imports(); - - if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { - module_or_path = fetch(module_or_path); - } - - __wbg_init_memory(imports); - - const { instance, module } = await __wbg_load(await module_or_path, imports); - - return __wbg_finalize_init(instance, module); -} - -export { initSync }; -export default __wbg_init; -``` -./dist/pubky_app_specs.d.ts -``` -/* tslint:disable */ -/* eslint-disable */ -export function create_pubky_app_user(name: string, bio: string | null | undefined, image: string | null | undefined, links: any, status?: string | null): any; -export function create_pubky_app_feed(tags: any, reach: string, layout: string, sort: string, content: string | null | undefined, name: string): any; -export function create_pubky_app_file(name: string, src: string, content_type: string, size: bigint): any; -export function create_pubky_app_post(content: string, kind: string, parent: string | null | undefined, embed: any, attachments: any): any; -export function create_pubky_app_tag(uri: string, label: string): any; -export function create_pubky_app_bookmark(uri: string): any; -export function create_pubky_app_follow(pubky_id: string): any; -export function create_pubky_app_mute(pubky_id: string): any; -export function create_pubky_app_last_read(): any; -/** - * Represents a user's single link with a title and URL. - */ -export class PubkyAppSpecs { - free(): void; - /** - * Creates a new `PubkyAppSpecs` instance. - */ - constructor(pubky_id: string); - createFollow(followee_id: string): any; -} -/** - * URI: /pub/pubky.app/profile.json - */ -export class PubkyAppUser { - free(): void; - /** - * Creates a new `PubkyAppUser` instance and sanitizes it. - */ - constructor(name: string, bio?: string | null, image?: string | null, links?: PubkyAppUserLink[] | null, status?: string | null); - get_data(): any; -} -/** - * Represents a user's single link with a title and URL. - */ -export class PubkyAppUserLink { - free(): void; - /** - * Creates a new `PubkyAppUserLink` instance and sanitizes it. - */ - constructor(title: string, url: string); -} - -export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; - -export interface InitOutput { - readonly memory: WebAssembly.Memory; - readonly __wbg_pubkyappuser_free: (a: number, b: number) => void; - readonly __wbg_pubkyappuserlink_free: (a: number, b: number) => void; - readonly pubkyappuser_new: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => number; - readonly pubkyappuser_get_data: (a: number) => [number, number, number]; - readonly pubkyappuserlink_new: (a: number, b: number, c: number, d: number) => number; - readonly __wbg_pubkyappspecs_free: (a: number, b: number) => void; - readonly pubkyappspecs_new: (a: number, b: number) => number; - readonly pubkyappspecs_createFollow: (a: number, b: number, c: number) => [number, number, number]; - readonly create_pubky_app_user: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: number, i: number) => [number, number, number]; - readonly create_pubky_app_feed: (a: any, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number, number]; - readonly create_pubky_app_file: (a: number, b: number, c: number, d: number, e: number, f: number, g: bigint) => [number, number, number]; - readonly create_pubky_app_post: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: any) => [number, number, number]; - readonly create_pubky_app_tag: (a: number, b: number, c: number, d: number) => [number, number, number]; - readonly create_pubky_app_bookmark: (a: number, b: number) => [number, number, number]; - readonly create_pubky_app_follow: (a: number, b: number) => [number, number, number]; - readonly create_pubky_app_mute: (a: number, b: number) => [number, number, number]; - readonly create_pubky_app_last_read: () => [number, number, number]; - readonly __wbindgen_malloc: (a: number, b: number) => number; - readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; - readonly __wbindgen_exn_store: (a: number) => void; - readonly __externref_table_alloc: () => number; - readonly __wbindgen_export_4: WebAssembly.Table; - readonly __externref_table_dealloc: (a: number) => void; - readonly __wbindgen_start: () => void; -} - -export type SyncInitInput = BufferSource | WebAssembly.Module; -/** -* Instantiates the given `module`, which can either be bytes or -* a precompiled `WebAssembly.Module`. -* -* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. -* -* @returns {InitOutput} -*/ -export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; - -/** -* If `module_or_path` is {RequestInfo} or {URL}, makes a request and -* for everything else, calls `WebAssembly.instantiate` directly. -* -* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. -* -* @returns {Promise} -*/ -export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; -``` -./dist/README.md -``` -# TODO -``` -./dist/package.json -``` -{ - "name": "pubky-app-specs", - "type": "module", - "description": "Pubky.app Data Model Specifications", - "version": "0.3.0", - "license": "MIT", - "collaborators": [ - "SHAcollision" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/pubky/pubky-app-specs.git" - }, - "files": [ - "pubky_app_specs_bg.wasm", - "pubky_app_specs.js", - "pubky_app_specs.d.ts" - ], - "main": "pubky_app_specs.js", - "homepage": "https://pubky.app", - "types": "pubky_app_specs.d.ts", - "sideEffects": [ - "./snippets/*" - ] -} -``` -./src/common.rs -``` -pub static VERSION: &str = "0.3.0"; -pub static APP_PATH: &str = "/pub/pubky.app/"; -pub static PROTOCOL: &str = "pubky://"; - -#[cfg(target_arch = "wasm32")] -use js_sys::Date; - -/// Returns the current timestamp in microseconds since the UNIX epoch. -#[cfg(target_arch = "wasm32")] -pub fn timestamp() -> i64 { - // Use JS Date.now() which returns ms since Unix epoch - let ms = Date::now() as i64; - // Convert to microseconds if you like - ms * 1_000 -} - -#[cfg(not(target_arch = "wasm32"))] -use std::time::{SystemTime, UNIX_EPOCH}; - -#[cfg(not(target_arch = "wasm32"))] -pub fn timestamp() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_micros() as i64 -} -``` -./src/traits.rs -``` -use crate::common::timestamp; -use base32::{decode, encode, Alphabet}; -use blake3::Hasher; -use serde::de::DeserializeOwned; - -#[cfg(target_arch = "wasm32")] -use serde::Serialize; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -pub trait TimestampId { - /// Creates a unique identifier based on the current timestamp. - fn create_id(&self) -> String { - // Get current time in microseconds since UNIX epoch - let now = timestamp(); - - // Convert to big-endian bytes - let bytes = now.to_be_bytes(); - - // Encode the bytes using Base32 with the Crockford alphabet - encode(Alphabet::Crockford, &bytes) - } - - /// Validates that the provided ID is a valid Crockford Base32-encoded timestamp, - /// 13 characters long, and represents a reasonable timestamp. - fn validate_id(&self, id: &str) -> Result<(), String> { - // Ensure ID is 13 characters long - if id.len() != 13 { - return Err("Validation Error: Invalid ID length: must be 13 characters".into()); - } - - // Decode the Crockford Base32-encoded ID - let decoded_bytes = - decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?; - - if decoded_bytes.len() != 8 { - return Err("Validation Error: Invalid ID length after decoding".into()); - } - - // Convert the decoded bytes to a timestamp in microseconds - let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap()); - - // Get current time in microseconds - let now_micros = timestamp(); - - // Define October 1st, 2024, in microseconds since UNIX epoch - let oct_first_2024_micros = 1727740800000000; // Timestamp for 2024-10-01 00:00:00 UTC - - // Allowable future duration (2 hours) in microseconds - let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000; - - // Validate that the ID's timestamp is after October 1st, 2024 - if timestamp_micros < oct_first_2024_micros { - return Err( - "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(), - ); - } - - // Validate that the ID's timestamp is not more than 2 hours in the future - if timestamp_micros > max_future_micros { - return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); - } - - Ok(()) - } -} - -/// Trait for generating an ID based on the struct's data. -pub trait HashId { - fn get_id_data(&self) -> String; - - /// Creates a unique identifier for bookmarks and tag homeserver paths instance. - /// - /// The ID is generated by: - /// 1. Concatenating the `uri` and `label` fields of the `PubkyAppTag` with a colon (`:`) separator. - /// 2. Hashing the concatenated string using the `blake3` hashing algorithm. - /// 3. Taking the first half of the bytes from the resulting `blake3` hash. - /// 4. Encoding those bytes using the Crockford alphabet (Base32 variant). - /// - /// The resulting Crockford-encoded string is returned as the tag ID. - /// - /// # Returns - /// - A `String` representing the Crockford-encoded tag ID derived from the `blake3` hash of the concatenated `uri` and `label`. - fn create_id(&self) -> String { - let data = self.get_id_data(); - - // Create a Blake3 hash of the input data - let mut hasher = Hasher::new(); - hasher.update(data.as_bytes()); - let blake3_hash = hasher.finalize(); - - // Get the first half of the hash bytes - let half_hash_length = blake3_hash.as_bytes().len() / 2; - let half_hash = &blake3_hash.as_bytes()[..half_hash_length]; - - // Encode the first half of the hash in Base32 using the Z-base32 alphabet - encode(Alphabet::Crockford, half_hash) - } - - /// Validates that the provided ID matches the generated ID. - fn validate_id(&self, id: &str) -> Result<(), String> { - let generated_id = self.create_id(); - if generated_id != id { - return Err(format!( - "Invalid ID: expected {}, found {}", - generated_id, id - )); - } - Ok(()) - } -} - -pub trait Validatable: Sized + DeserializeOwned { - fn try_from(blob: &[u8], id: &str) -> Result { - let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?; - instance = instance.sanitize(); - instance.validate(id)?; - Ok(instance) - } - - fn validate(&self, id: &str) -> Result<(), String>; - - fn sanitize(self) -> Self { - self - } -} - -pub trait HasPath { - fn create_path(&self) -> String; -} - -pub trait HasPubkyIdPath { - fn create_path(&self, pubky_id: &str) -> String; -} - -#[cfg(target_arch = "wasm32")] -pub trait JSdata: HasPath + Serialize { - // helper that returns { id, path, json } - fn get_data(&self) -> Result { - let path = self.create_path(); - - let json_val = serde_wasm_bindgen::to_value(&self) - .map_err(|e| JsValue::from_str(&format!("JSON Error: {}", e)))?; - - // Construct a small JS object - let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &JsValue::from_str("id"), &JsValue::null())?; - js_sys::Reflect::set(&obj, &JsValue::from_str("path"), &JsValue::from_str(&path))?; - js_sys::Reflect::set(&obj, &JsValue::from_str("json"), &json_val)?; - - Ok(obj.into()) - } -} -``` -./src/wasm.rs -``` -use crate::traits::{HasPath, HasPubkyIdPath, HashId, TimestampId, Validatable}; -use crate::*; -use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::{from_value, to_value}; -use wasm_bindgen::prelude::*; - -/// Each FFI function: -/// - Accepts minimal fields in a JavaScript-friendly manner (e.g. strings, JSON). -/// - Creates the Rust model, sanitizes, and validates it. -/// - Generates the ID (if applicable). -/// - Generates the path (if applicable). -/// - Returns { json, id, path } or a descriptive error. - -/// Returned by each FFI function to JS. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CreateResult { - /// The fully validated and sanitized object, as JSON - json: serde_json::Value, - /// The unique ID for this object (empty if none) - id: String, - /// The final path (or empty if none) - path: String, - /// The final url (or empty if none) - url: String, -} - -/// Convert a Rust object into `{ json, id, path, url }` for JS. -fn build_create_result(obj: &T, id: &str, path: &str) -> Result -where - T: Serialize, -{ - // 1) Serialize `obj` into a JavaScript object - let json_val = to_value(obj).map_err(|e| JsValue::from_str(&format!("JSON Error: {}", e)))?; - - // 2) Construct the final object { id, path, json } just like `get_data()` does - let ret_obj = js_sys::Object::new(); - js_sys::Reflect::set(&ret_obj, &JsValue::from_str("id"), &JsValue::from_str(id))?; - js_sys::Reflect::set( - &ret_obj, - &JsValue::from_str("path"), - &JsValue::from_str(path), - )?; - js_sys::Reflect::set(&ret_obj, &JsValue::from_str("json"), &json_val)?; - - Ok(ret_obj.into()) -} - -/// Represents a user's single link with a title and URL. -#[wasm_bindgen] -pub struct PubkyAppSpecs { - #[wasm_bindgen(skip)] - pub pubky_id: String, -} - -#[wasm_bindgen] -impl PubkyAppSpecs { - /// Creates a new `PubkyAppSpecs` instance. - #[wasm_bindgen(constructor)] - pub fn new(pubky_id: String) -> Self { - Self { pubky_id } - } - - fn create_url(&self, path: &str) -> String { - format!("pubky://{}{}", self.pubky_id, path) - } - - /// Convert a Rust object into `{ json, id, path, url }` for JS. - fn to_js(&self, obj: &T, id: &str, path: &str) -> Result - where - T: Serialize, - { - let url = self.create_url(path); - // 1) Serialize `obj` into a JavaScript object - let json_val = - to_value(obj).map_err(|e| JsValue::from_str(&format!("JSON Error: {}", e)))?; - - // 2) Construct the final object { id, path, json } just like `get_data()` does - let ret_obj = js_sys::Object::new(); - js_sys::Reflect::set(&ret_obj, &JsValue::from_str("id"), &JsValue::from_str(id))?; - js_sys::Reflect::set( - &ret_obj, - &JsValue::from_str("path"), - &JsValue::from_str(path), - )?; - js_sys::Reflect::set( - &ret_obj, - &JsValue::from_str("url"), - &JsValue::from_str(&url), - )?; - js_sys::Reflect::set(&ret_obj, &JsValue::from_str("json"), &json_val)?; - - Ok(ret_obj.into()) - } - - #[wasm_bindgen(js_name = createFollow)] - pub fn create_follow(&self, followee_id: String) -> Result { - let follow = PubkyAppFollow::new(); - follow.validate(&followee_id)?; // No ID in follow, so we pass user ID or empty - - // Path requires the user ID - let path = follow.create_path(&followee_id); - - // Return an empty ID for follow - self.to_js(&follow, "", &path) - } -} - -// ----------------------------------------------------------------------------- -// 1. PubkyAppUser -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_user( - name: String, - bio: Option, - image: Option, - links: JsValue, // JSON array of PubkyAppUserLink - status: Option, -) -> Result { - // Convert links to Option> - let links_vec: Option> = if links.is_null() || links.is_undefined() { - None - } else { - from_value(links)? - }; - - // Create user, sanitize, then validate - let user = PubkyAppUser::new(name, bio, image, links_vec, status); - user.validate("")?; // no ID-based validation - - // We have no ID for PubkyAppUser. The path is always profile.json - let path = user.create_path(); - - build_create_result(&user, "", &path) -} - -// ----------------------------------------------------------------------------- -// 2. PubkyAppFeed -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_feed( - tags: JsValue, // JSON array of strings - reach: String, - layout: String, - sort: String, - content: Option, - name: String, -) -> Result { - // Convert tags - let tags_vec: Option> = if tags.is_null() || tags.is_undefined() { - None - } else { - from_value(tags)? - }; - - // Convert feed reach - let reach_enum = match reach.as_str() { - "following" => PubkyAppFeedReach::Following, - "followers" => PubkyAppFeedReach::Followers, - "friends" => PubkyAppFeedReach::Friends, - "all" => PubkyAppFeedReach::All, - _ => return Err(JsValue::from_str("Invalid feed reach")), - }; - - // Convert layout - let layout_enum = match layout.as_str() { - "columns" => PubkyAppFeedLayout::Columns, - "wide" => PubkyAppFeedLayout::Wide, - "visual" => PubkyAppFeedLayout::Visual, - _ => return Err(JsValue::from_str("Invalid feed layout")), - }; - - // Convert sort - let sort_enum = match sort.as_str() { - "recent" => PubkyAppFeedSort::Recent, - "popularity" => PubkyAppFeedSort::Popularity, - _ => return Err(JsValue::from_str("Invalid feed sort")), - }; - - // Convert content kind - let content_kind = match content.as_deref() { - Some("short") => Some(PubkyAppPostKind::Short), - Some("long") => Some(PubkyAppPostKind::Long), - Some("image") => Some(PubkyAppPostKind::Image), - Some("video") => Some(PubkyAppPostKind::Video), - Some("link") => Some(PubkyAppPostKind::Link), - Some("file") => Some(PubkyAppPostKind::File), - None => None, - Some(_) => return Err(JsValue::from_str("Invalid content kind")), - }; - - // Build feed, sanitize, validate - let feed = PubkyAppFeed::new( - tags_vec, - reach_enum, - layout_enum, - sort_enum, - content_kind, - name, - ); - let feed_id = feed.create_id(); - feed.validate(&feed_id)?; - - let path = feed.create_path(); - build_create_result(&feed, &feed_id, &path) -} - -// ----------------------------------------------------------------------------- -// 3. PubkyAppFile -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_file( - name: String, - src: String, - content_type: String, - size: i64, -) -> Result { - let file = PubkyAppFile::new(name, src, content_type, size); - let file_id = file.create_id(); - file.validate(&file_id)?; - - let path = file.create_path(); - build_create_result(&file, &file_id, &path) -} - -// ----------------------------------------------------------------------------- -// 4. PubkyAppPost -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_post( - content: String, - kind: String, - parent: Option, - embed: JsValue, // JSON object { kind: string, uri: string } or null - attachments: JsValue, // JSON array of string or null -) -> Result { - // Convert kind - let kind_enum = match kind.as_str() { - "short" => PubkyAppPostKind::Short, - "long" => PubkyAppPostKind::Long, - "image" => PubkyAppPostKind::Image, - "video" => PubkyAppPostKind::Video, - "link" => PubkyAppPostKind::Link, - "file" => PubkyAppPostKind::File, - _ => return Err(JsValue::from_str("Invalid post kind")), - }; - - // Convert embed - let embed_option: Option = if embed.is_null() || embed.is_undefined() { - None - } else { - from_value(embed)? - }; - - // Convert attachments - let attachments_vec: Option> = - if attachments.is_null() || attachments.is_undefined() { - None - } else { - from_value(attachments)? - }; - - // Build the post, sanitize, validate - let post = PubkyAppPost::new(content, kind_enum, parent, embed_option, attachments_vec); - let post_id = post.create_id(); - post.validate(&post_id)?; - - let path = post.create_path(); - build_create_result(&post, &post_id, &path) -} - -// ----------------------------------------------------------------------------- -// 5. PubkyAppTag -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_tag(uri: String, label: String) -> Result { - let tag = PubkyAppTag::new(uri, label); - let tag_id = tag.create_id(); - tag.validate(&tag_id)?; - - let path = tag.create_path(); - build_create_result(&tag, &tag_id, &path) -} - -// ----------------------------------------------------------------------------- -// 6. PubkyAppBookmark -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_bookmark(uri: String) -> Result { - let bookmark = PubkyAppBookmark::new(uri); - let bookmark_id = bookmark.create_id(); - bookmark.validate(&bookmark_id)?; - - let path = bookmark.create_path(); - build_create_result(&bookmark, &bookmark_id, &path) -} - -// ----------------------------------------------------------------------------- -// 7. PubkyAppFollow -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_follow(pubky_id: String) -> Result { - let follow = PubkyAppFollow::new(); - follow.validate(&pubky_id)?; // No ID in follow, so we pass user ID or empty - - // Path requires the user ID - let path = follow.create_path(&pubky_id); - - // Return an empty ID for follow - build_create_result(&follow, "", &path) -} - -// ----------------------------------------------------------------------------- -// 8. PubkyAppMute -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_mute(pubky_id: String) -> Result { - let mute = PubkyAppMute::new(); - mute.validate(&pubky_id)?; - - let path = mute.create_path(&pubky_id); - build_create_result(&mute, "", &path) -} - -// ----------------------------------------------------------------------------- -// 9. PubkyAppLastRead -// ----------------------------------------------------------------------------- - -#[wasm_bindgen] -pub fn create_pubky_app_last_read() -> Result { - let last_read = PubkyAppLastRead::new(); - last_read.validate("")?; - - let path = last_read.create_path(); - build_create_result(&last_read, "", &path) -} - -// ----------------------------------------------------------------------------- -// 10. PubkyAppBlob -// ----------------------------------------------------------------------------- - -/// A small wrapper for JSON-serializing the blob data as base64. -#[derive(Serialize, Deserialize, Clone)] -pub struct PubkyAppBlobJson { - pub data_base64: String, -} - -// #[wasm_bindgen] -// pub fn create_pubky_app_blob(blob_data: JsValue) -> Result { -// // 1) Convert from JsValue (Uint8Array in JS) -> Vec in Rust -// let data_vec: Vec = from_value(blob_data) -// .map_err(|e| JsValue::from_str(&format!("Invalid blob bytes: {}", e)))?; - -// // 2) Build the PubkyAppBlob -// let blob = PubkyAppBlob(data_vec); - -// // 3) Generate ID and path -// let id = blob.create_id(); -// let path = blob.create_path(); - -// // 4) Provide a minimal JSON representation (e.g. base64) -// let json_blob = PubkyAppBlobJson { -// data_base64: base64::encode(&blob.0), -// }; - -// // 5) Return { json, id, path } -// build_create_result(&json_blob, &id, &path) -// } -``` -./src/lib.rs -``` -mod common; -mod models; -pub mod traits; - -// Re-export domain types -pub use common::{APP_PATH, PROTOCOL, VERSION}; -pub use models::bookmark::PubkyAppBookmark; -pub use models::feed::{PubkyAppFeed, PubkyAppFeedLayout, PubkyAppFeedReach, PubkyAppFeedSort}; -pub use models::file::PubkyAppFile; -pub use models::file_blob::PubkyAppBlob; -pub use models::follow::PubkyAppFollow; -pub use models::last_read::PubkyAppLastRead; -pub use models::mute::PubkyAppMute; -pub use models::post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; -pub use models::tag::PubkyAppTag; -pub use models::user::{PubkyAppUser, PubkyAppUserLink}; - -// Our WASM module -#[cfg(target_arch = "wasm32")] -mod wasm; -// Re-export the Wasm functions so they're available to wasm-pack -#[cfg(target_arch = "wasm32")] -pub use wasm::*; -``` -./src/models/feed.rs -``` -use crate::{ - common::timestamp, - traits::{HasPath, HashId, Validatable}, - PubkyAppPostKind, APP_PATH, -}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Enum representing the reach of the feed. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub enum PubkyAppFeedReach { - Following, - Followers, - Friends, - All, -} - -/// Enum representing the layout of the feed. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub enum PubkyAppFeedLayout { - Columns, - Wide, - Visual, -} - -/// Enum representing the sort order of the feed. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub enum PubkyAppFeedSort { - Recent, - Popularity, -} - -/// Configuration object for the feed. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppFeedConfig { - pub tags: Option>, - pub reach: PubkyAppFeedReach, - pub layout: PubkyAppFeedLayout, - pub sort: PubkyAppFeedSort, - pub content: Option, -} - -/// Represents a feed configuration. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppFeed { - pub feed: PubkyAppFeedConfig, - pub name: String, - pub created_at: i64, -} - -impl PubkyAppFeed { - /// Creates a new `PubkyAppFeed` instance and sanitizes it. - pub fn new( - tags: Option>, - reach: PubkyAppFeedReach, - layout: PubkyAppFeedLayout, - sort: PubkyAppFeedSort, - content: Option, - name: String, - ) -> Self { - let created_at = timestamp(); - let feed = PubkyAppFeedConfig { - tags, - reach, - layout, - sort, - content, - }; - Self { - feed, - name, - created_at, - } - .sanitize() - } -} - -impl HashId for PubkyAppFeed { - /// Generates an ID based on the serialized `feed` object. - fn get_id_data(&self) -> String { - serde_json::to_string(&self.feed).unwrap_or_default() - } -} - -impl HasPath for PubkyAppFeed { - fn create_path(&self) -> String { - format!("{}feeds/{}", APP_PATH, self.create_id()) - } -} - -impl Validatable for PubkyAppFeed { - fn validate(&self, id: &str) -> Result<(), String> { - self.validate_id(id)?; - - // Validate name - if self.name.trim().is_empty() { - return Err("Validation Error: Feed name cannot be empty".into()); - } - - // Additional validations can be added here - Ok(()) - } - - fn sanitize(self) -> Self { - // Sanitize name - let name = self.name.trim().to_string(); - - // Sanitize tags - let feed = PubkyAppFeedConfig { - tags: self.feed.tags.map(|tags| { - tags.into_iter() - .map(|tag| tag.trim().to_lowercase()) - .collect() - }), - ..self.feed - }; - - PubkyAppFeed { - feed, - name, - created_at: self.created_at, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_new() { - let feed = PubkyAppFeed::new( - Some(vec!["bitcoin".to_string(), "rust".to_string()]), - PubkyAppFeedReach::Following, - PubkyAppFeedLayout::Columns, - PubkyAppFeedSort::Recent, - Some(PubkyAppPostKind::Image), - "Rust Bitcoiners".to_string(), - ); - - let feed_config = PubkyAppFeedConfig { - tags: Some(vec!["bitcoin".to_string(), "rust".to_string()]), - reach: PubkyAppFeedReach::Following, - layout: PubkyAppFeedLayout::Columns, - sort: PubkyAppFeedSort::Recent, - content: Some(PubkyAppPostKind::Image), - }; - assert_eq!(feed.feed, feed_config); - assert_eq!(feed.name, "Rust Bitcoiners"); - // Check that created_at is recent - let now = timestamp(); - assert!(feed.created_at <= now && feed.created_at >= now - 1_000_000); - } - - #[test] - fn test_create_id() { - let feed = PubkyAppFeed::new( - Some(vec!["bitcoin".to_string(), "rust".to_string()]), - PubkyAppFeedReach::Following, - PubkyAppFeedLayout::Columns, - PubkyAppFeedSort::Recent, - None, - "Rust Bitcoiners".to_string(), - ); - - let feed_id = feed.create_id(); - println!("Feed ID: {}", feed_id); - // The ID should not be empty - assert!(!feed_id.is_empty()); - } - - #[test] - fn test_validate() { - let feed = PubkyAppFeed::new( - Some(vec!["bitcoin".to_string(), "rust".to_string()]), - PubkyAppFeedReach::Following, - PubkyAppFeedLayout::Columns, - PubkyAppFeedSort::Recent, - None, - "Rust Bitcoiners".to_string(), - ); - let feed_id = feed.create_id(); - - let result = feed.validate(&feed_id); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_id() { - let feed = PubkyAppFeed::new( - Some(vec!["bitcoin".to_string(), "rust".to_string()]), - PubkyAppFeedReach::Following, - PubkyAppFeedLayout::Columns, - PubkyAppFeedSort::Recent, - None, - "Rust Bitcoiners".to_string(), - ); - let invalid_id = "INVALIDID"; - let result = feed.validate(invalid_id); - assert!(result.is_err()); - } - - #[test] - fn test_sanitize() { - let feed = PubkyAppFeed::new( - Some(vec![" BiTcoin ".to_string(), " RUST ".to_string()]), - PubkyAppFeedReach::Following, - PubkyAppFeedLayout::Columns, - PubkyAppFeedSort::Recent, - None, - " Rust Bitcoiners".to_string(), - ); - assert_eq!(feed.name, "Rust Bitcoiners"); - assert_eq!( - feed.feed.tags, - Some(vec!["bitcoin".to_string(), "rust".to_string()]) - ); - } - - #[test] - fn test_try_from_valid() { - let feed_json = r#" - { - "feed": { - "tags": ["bitcoin", "rust"], - "reach": "following", - "layout": "columns", - "sort": "recent", - "content": "video" - }, - "name": "My Feed", - "created_at": 1700000000 - } - "#; - - let feed: PubkyAppFeed = serde_json::from_str(feed_json).unwrap(); - let feed_id = feed.create_id(); - - let blob = feed_json.as_bytes(); - let feed_parsed = ::try_from(blob, &feed_id).unwrap(); - - assert_eq!(feed_parsed.name, "My Feed"); - assert_eq!( - feed_parsed.feed.tags, - Some(vec!["bitcoin".to_string(), "rust".to_string()]) - ); - } -} -``` -./src/models/user.rs -``` -use crate::{ - traits::{HasPath, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[cfg(target_arch = "wasm32")] -use crate::traits::JSdata; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -// Validation constants -const MIN_USERNAME_LENGTH: usize = 3; -const MAX_USERNAME_LENGTH: usize = 50; -const MAX_BIO_LENGTH: usize = 160; -const MAX_IMAGE_LENGTH: usize = 300; -const MAX_LINKS: usize = 5; -const MAX_LINK_TITLE_LENGTH: usize = 100; -const MAX_LINK_URL_LENGTH: usize = 300; -const MAX_STATUS_LENGTH: usize = 50; - -/// URI: /pub/pubky.app/profile.json -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppUser { - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - // Avoid wasm-pack automatically generating getter/setters for the pub fields. - pub name: String, - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub bio: Option, - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub image: Option, - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub links: Option>, - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub status: Option, -} - -/// Represents a user's single link with a title and URL. -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -#[derive(Serialize, Deserialize, Default, Clone, Debug)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppUserLink { - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub title: String, - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] - pub url: String, -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -impl PubkyAppUser { - /// Creates a new `PubkyAppUser` instance and sanitizes it. - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] - pub fn new( - name: String, - bio: Option, - image: Option, - links: Option>, - status: Option, - ) -> Self { - Self { - name, - bio, - image, - links, - status, - } - .sanitize() - } - - #[cfg(target_arch = "wasm32")] - pub fn get_data(&self) -> Result { - JSdata::get_data(self) - } -} - -#[cfg(target_arch = "wasm32")] -impl JSdata for PubkyAppUser {} - -impl HasPath for PubkyAppUser { - fn create_path(&self) -> String { - format!("{}profile.json", APP_PATH) - } -} - -impl Validatable for PubkyAppUser { - fn sanitize(self) -> Self { - // Sanitize name - let sanitized_name = self.name.trim(); - // Crop name to a maximum length of MAX_USERNAME_LENGTH characters - let mut name = sanitized_name - .chars() - .take(MAX_USERNAME_LENGTH) - .collect::(); - - // We use username keyword `[DELETED]` for a user whose `profile.json` has been deleted - // Therefore this is not a valid username. - if name == *"[DELETED]" { - name = "anonymous".to_string(); // default username - } - - // Sanitize bio - let bio = self - .bio - .map(|b| b.trim().chars().take(MAX_BIO_LENGTH).collect::()); - - // Sanitize image URL with URL parsing - let image = match &self.image { - Some(image_url) => { - let sanitized_image_url = image_url.trim(); - - match Url::parse(sanitized_image_url) { - Ok(_) => { - // Ensure the URL is within the allowed limit - let url = sanitized_image_url - .chars() - .take(MAX_IMAGE_LENGTH) - .collect::(); - Some(url) // Valid image URL - } - Err(_) => None, // Invalid image URL, set to None - } - } - None => None, - }; - - // Sanitize status - let status = self - .status - .map(|s| s.trim().chars().take(MAX_STATUS_LENGTH).collect::()); - - // Sanitize links - let links = self.links.map(|links_vec| { - links_vec - .into_iter() - .take(MAX_LINKS) - .map(|link| link.sanitize()) - .filter(|link| !link.url.is_empty()) - .collect() - }); - - PubkyAppUser { - name, - bio, - image, - links, - status, - } - } - - fn validate(&self, _id: &str) -> Result<(), String> { - // Validate name length - let name_length = self.name.chars().count(); - if !(MIN_USERNAME_LENGTH..=MAX_USERNAME_LENGTH).contains(&name_length) { - return Err("Validation Error: Invalid name length".into()); - } - - // Validate bio length - if let Some(bio) = &self.bio { - if bio.chars().count() > MAX_BIO_LENGTH { - return Err("Validation Error: Bio exceeds maximum length".into()); - } - } - - // Validate image length - if let Some(image) = &self.image { - if image.chars().count() > MAX_IMAGE_LENGTH { - return Err("Validation Error: Image URI exceeds maximum length".into()); - } - } - - // Validate links - if let Some(links) = &self.links { - if links.len() > MAX_LINKS { - return Err("Too many links".to_string()); - } - - for link in links { - link.validate(_id)?; - } - } - - // Validate status length - if let Some(status) = &self.status { - if status.chars().count() > MAX_STATUS_LENGTH { - return Err("Validation Error: Status exceeds maximum length".into()); - } - } - - Ok(()) - } -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -impl PubkyAppUserLink { - /// Creates a new `PubkyAppUserLink` instance and sanitizes it. - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] - pub fn new(title: String, url: String) -> Self { - Self { title, url }.sanitize() - } -} - -impl Validatable for PubkyAppUserLink { - fn sanitize(self) -> Self { - let title = self - .title - .trim() - .chars() - .take(MAX_LINK_TITLE_LENGTH) - .collect::(); - - let url = match Url::parse(self.url.trim()) { - Ok(parsed_url) => { - let sanitized_url = parsed_url.to_string(); - sanitized_url - .chars() - .take(MAX_LINK_URL_LENGTH) - .collect::() - } - Err(_) => "".to_string(), // Default to empty string for invalid URLs - }; - - PubkyAppUserLink { title, url } - } - - fn validate(&self, _id: &str) -> Result<(), String> { - if self.title.chars().count() > MAX_LINK_TITLE_LENGTH { - return Err("Validation Error: Link title exceeds maximum length".to_string()); - } - - if self.url.chars().count() > MAX_LINK_URL_LENGTH { - return Err("Validation Error: Link URL exceeds maximum length".to_string()); - } - - match Url::parse(&self.url) { - Ok(_) => Ok(()), - Err(_) => Err("Validation Error: Invalid URL format".to_string()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - use crate::APP_PATH; - - #[test] - fn test_new() { - let user = PubkyAppUser::new( - "Alice".to_string(), - Some("Maximalist".to_string()), - Some("https://example.com/image.png".to_string()), - Some(vec![ - PubkyAppUserLink { - title: "GitHub".to_string(), - url: "https://github.com/alice".to_string(), - }, - PubkyAppUserLink { - title: "Website".to_string(), - url: "https://alice.dev".to_string(), - }, - ]), - Some("Exploring the decentralized web.".to_string()), - ); - - assert_eq!(user.name, "Alice"); - assert_eq!(user.bio.as_deref(), Some("Maximalist")); - assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); - assert_eq!( - user.status.as_deref(), - Some("Exploring the decentralized web.") - ); - assert!(user.links.is_some()); - assert_eq!(user.links.as_ref().unwrap().len(), 2); - } - - #[test] - fn test_create_path() { - let user = PubkyAppUser::default(); - let path = user.create_path(); - assert_eq!(path, format!("{}profile.json", APP_PATH)); - } - - #[test] - fn test_sanitize() { - let user = PubkyAppUser::new( - " Alice ".to_string(), - Some(" Maximalist and developer. ".to_string()), - Some("https://example.com/image.png".to_string()), - Some(vec![ - PubkyAppUserLink { - title: " GitHub ".to_string(), - url: " https://github.com/alice ".to_string(), - }, - PubkyAppUserLink { - title: "Website".to_string(), - url: "invalid_url".to_string(), // Invalid URL - }, - ]), - Some(" Exploring the decentralized web. ".to_string()), - ); - - assert_eq!(user.name, "Alice"); - assert_eq!(user.bio.as_deref(), Some("Maximalist and developer.")); - assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); - assert_eq!( - user.status.as_deref(), - Some("Exploring the decentralized web.") - ); - assert!(user.links.is_some()); - let links = user.links.unwrap(); - assert_eq!(links.len(), 1); // Invalid URL link should be filtered out - assert_eq!(links[0].title, "GitHub"); - assert_eq!(links[0].url, "https://github.com/alice"); - } - - #[test] - fn test_validate_valid() { - let user = PubkyAppUser::new( - "Alice".to_string(), - Some("Maximalist".to_string()), - Some("https://example.com/image.png".to_string()), - None, - Some("Exploring the decentralized web.".to_string()), - ); - - let result = user.validate(""); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_name() { - let user = PubkyAppUser::new( - "Al".to_string(), // Too short - None, - None, - None, - None, - ); - - let result = user.validate(""); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Validation Error: Invalid name length" - ); - } - - #[test] - fn test_try_from_valid() { - let user_json = r#" - { - "name": "Alice", - "bio": "Maximalist", - "image": "https://example.com/image.png", - "links": [ - { - "title": "GitHub", - "url": "https://github.com/alice" - }, - { - "title": "Website", - "url": "https://alice.dev" - } - ], - "status": "Exploring the decentralized web." - } - "#; - - let blob = user_json.as_bytes(); - let user = ::try_from(blob, "").unwrap(); - - assert_eq!(user.name, "Alice"); - assert_eq!(user.bio.as_deref(), Some("Maximalist")); - assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); - assert_eq!( - user.status.as_deref(), - Some("Exploring the decentralized web.") - ); - assert!(user.links.is_some()); - assert_eq!(user.links.as_ref().unwrap().len(), 2); - } - - #[test] - fn test_try_from_invalid_link() { - let user_json = r#" - { - "name": "Alice", - "links": [ - { - "title": "GitHub", - "url": "invalid_url" - } - ] - } - "#; - - let blob = user_json.as_bytes(); - let user = ::try_from(blob, "").unwrap(); - - // Since the link URL is invalid, it should be filtered out - assert!(user.links.is_none() || user.links.as_ref().unwrap().is_empty()); - } -} -``` -./src/models/file_blob.rs -``` -use crate::{ - traits::{HasPath, HashId}, - APP_PATH, -}; - -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -const SAMPLE_SIZE: usize = 2 * 1024; - -/// Represents a file uploaded by the user. -/// URI: /pub/pubky.app/files/:file_id -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppBlob(pub Vec); - -impl HashId for PubkyAppBlob { - fn get_id_data(&self) -> String { - // Get the start and end samples - let start = &self.0[..SAMPLE_SIZE.min(self.0.len())]; - let end = if self.0.len() > SAMPLE_SIZE { - &self.0[self.0.len() - SAMPLE_SIZE..] - } else { - &[] - }; - - // Combine the samples - let mut combined = Vec::with_capacity(start.len() + end.len()); - combined.extend_from_slice(start); - combined.extend_from_slice(end); - - base32::encode(base32::Alphabet::Crockford, &combined) - } -} - -impl HasPath for PubkyAppBlob { - fn create_path(&self) -> String { - format!("{}blobs/{}", APP_PATH, self.create_id()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::HashId; - - #[test] - fn test_get_id_data_size_is_smaller_than_sample() { - let blob = PubkyAppBlob(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - let id = blob.get_id_data(); - assert_eq!(id, "041061050R3GG28A"); - } -} -``` -./src/models/last_read.rs -``` -use crate::{ - common::timestamp, - traits::{HasPath, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents the last read timestamp for notifications. -/// URI: /pub/pubky.app/last_read -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppLastRead { - pub timestamp: i64, // Unix epoch time in milliseconds -} - -impl PubkyAppLastRead { - /// Creates a new `PubkyAppLastRead` instance. - pub fn new() -> Self { - let timestamp = timestamp() / 1_000; // to millis - Self { timestamp } - } -} - -impl Validatable for PubkyAppLastRead { - fn validate(&self, _id: &str) -> Result<(), String> { - // Validate timestamp is a positive integer - if self.timestamp <= 0 { - return Err("Validation Error: Timestamp must be a positive integer".into()); - } - Ok(()) - } -} - -impl HasPath for PubkyAppLastRead { - fn create_path(&self) -> String { - format!("{}last_read", APP_PATH) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_new() { - let last_read = PubkyAppLastRead::new(); - let now = timestamp() / 1_000; - // within 1 second - assert!(last_read.timestamp <= now && last_read.timestamp >= now - 1_000); - } - - #[test] - fn test_create_path() { - let last_read = PubkyAppLastRead::new(); - let path = last_read.create_path(); - assert_eq!(path, format!("{}last_read", APP_PATH)); - } - - #[test] - fn test_validate() { - let last_read = PubkyAppLastRead::new(); - let result = last_read.validate(""); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_timestamp() { - let last_read = PubkyAppLastRead { timestamp: -1 }; - let result = last_read.validate(""); - assert!(result.is_err()); - } - - #[test] - fn test_try_from_valid() { - let last_read_json = r#" - { - "timestamp": 1700000000 - } - "#; - - let blob = last_read_json.as_bytes(); - let last_read = ::try_from(blob, "").unwrap(); - assert_eq!(last_read.timestamp, 1700000000); - } -} -``` -./src/models/mod.rs -``` -pub mod bookmark; -pub mod feed; -pub mod file; -pub mod file_blob; -pub mod follow; -pub mod last_read; -pub mod mute; -pub mod post; -pub mod tag; -pub mod user; -``` -./src/models/file.rs -``` -use std::str::FromStr; - -use crate::{ - common::timestamp, - traits::{HasPath, TimestampId, Validatable}, - APP_PATH, -}; -use mime::Mime; -use serde::{Deserialize, Serialize}; - -use url::Url; -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -const MIN_NAME_LENGTH: usize = 1; -const MAX_NAME_LENGTH: usize = 255; -const MAX_SRC_LENGTH: usize = 1024; -const MAX_SIZE: i64 = 10 * (1 << 20); // 10 MB - -const VALID_MIME_TYPES: &[&str] = &[ - "application/javascript", - "application/json", - "application/octet-stream", - "application/pdf", - "application/x-www-form-urlencoded", - "application/xml", - "application/zip", - "audio/mpeg", - "audio/wav", - "image/gif", - "image/jpeg", - "image/png", - "image/svg+xml", - "image/webp", - "multipart/form-data", - "text/css", - "text/html", - "text/plain", - "text/xml", - "video/mp4", - "video/mpeg", -]; - -/// Represents a file uploaded by the user. -/// URI: /pub/pubky.app/files/:file_id -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppFile { - pub name: String, - pub created_at: i64, - pub src: String, - pub content_type: String, - pub size: i64, -} - -impl PubkyAppFile { - /// Creates a new `PubkyAppFile` instance. - pub fn new(name: String, src: String, content_type: String, size: i64) -> Self { - let created_at = timestamp(); - Self { - name, - created_at, - src, - content_type, - size, - } - .sanitize() - } -} - -impl TimestampId for PubkyAppFile {} - -impl HasPath for PubkyAppFile { - fn create_path(&self) -> String { - format!("{}files/{}", APP_PATH, self.create_id()) - } -} - -impl Validatable for PubkyAppFile { - fn sanitize(self) -> Self { - let name = self.name.trim().chars().take(MAX_NAME_LENGTH).collect(); - - let sanitized_src = self - .src - .trim() - .chars() - .take(MAX_SRC_LENGTH) - .collect::(); - - let src = match Url::parse(&sanitized_src) { - Ok(_) => Some(sanitized_src), - Err(_) => None, // Invalid src URL, set to None - }; - - let content_type = self.content_type.trim().to_string(); - - Self { - name, - created_at: self.created_at, - src: src.unwrap_or("".to_string()), - content_type, - size: self.size, - } - } - - fn validate(&self, id: &str) -> Result<(), String> { - self.validate_id(id)?; - - // Validate name - let name_length = self.name.chars().count(); - - if !(MIN_NAME_LENGTH..=MAX_NAME_LENGTH).contains(&name_length) { - return Err("Validation Error: Invalid name length".into()); - } - - // Validate src - if self.src.chars().count() == 0 { - return Err("Validation Error: Invalid src".into()); - } - if self.src.chars().count() > MAX_SRC_LENGTH { - return Err("Validation Error: src exceeds maximum length".into()); - } - - // validate content type - match Mime::from_str(&self.content_type) { - Ok(mime) => { - if !VALID_MIME_TYPES.contains(&mime.essence_str()) { - return Err("Validation Error: Invalid content type".into()); - } - } - Err(_) => { - return Err("Validation Error: Invalid content type".into()); - } - } - - // Validate size - if self.size <= 0 || self.size > MAX_SIZE { - return Err("Validation Error: Invalid size".into()); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_new() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "image/png".to_string(), - 1024, - ); - assert_eq!(file.name, "example.png"); - assert_eq!(file.src, "pubky://user_id/pub/pubky.app/blobs/id"); - assert_eq!(file.content_type, "image/png"); - assert_eq!(file.size, 1024); - // Check that created_at is recent - let now = timestamp(); - assert!(file.created_at <= now && file.created_at >= now - 1_000_000); // within 1 second - } - - #[test] - fn test_create_path() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "image/png".to_string(), - 1024, - ); - let file_id = file.create_id(); - let path = file.create_path(); - - // Check if the path starts with the expected prefix - let prefix = format!("{}files/", APP_PATH); - assert!(path.starts_with(&prefix)); - - let expected_path_len = prefix.len() + file_id.len(); - assert_eq!(path.len(), expected_path_len); - } - - #[test] - fn test_validate_valid() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "image/png".to_string(), - 1024, - ); - let id = file.create_id(); - let result = file.validate(&id); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_id() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "image/png".to_string(), - 1024, - ); - let invalid_id = "INVALIDID"; - let result = file.validate(invalid_id); - assert!(result.is_err()); - } - - #[test] - fn test_validate_invalid_content_type() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "notavalid/content_type".to_string(), - 1024, - ); - let id = file.create_id(); - let result = file.validate(&id); - assert!(result.is_err()); - } - - #[test] - fn test_validate_invalid_size() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "notavalid/content_type".to_string(), - MAX_SIZE + 1, - ); - let id = file.create_id(); - let result = file.validate(&id); - assert!(result.is_err()); - } - - #[test] - fn test_validate_invalid_src() { - let file = PubkyAppFile::new( - "example.png".to_string(), - "not_a_url".to_string(), - "notavalid/content_type".to_string(), - MAX_SIZE + 1, - ); - let id = file.create_id(); - let result = file.validate(&id); - assert!(result.is_err()); - } - - #[test] - fn test_try_from_valid() { - let file_json = r#" - { - "name": "example.png", - "created_at": 1627849723, - "src": "pubky://user_id/pub/pubky.app/blobs/id", - "content_type": "image/png", - "size": 1024 - } - "#; - - let file = PubkyAppFile::new( - "example.png".to_string(), - "pubky://user_id/pub/pubky.app/blobs/id".to_string(), - "image/png".to_string(), - 1024, - ); - let id = file.create_id(); - - let blob = file_json.as_bytes(); - let file_parsed = ::try_from(blob, &id).unwrap(); - - assert_eq!(file_parsed.name, "example.png"); - assert_eq!(file_parsed.src, "pubky://user_id/pub/pubky.app/blobs/id"); - assert_eq!(file_parsed.content_type, "image/png"); - assert_eq!(file_parsed.size, 1024); - } -} -``` -./src/models/bookmark.rs -``` -use crate::{ - common::timestamp, - traits::{HasPath, HashId, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents raw homeserver bookmark with id -/// URI: /pub/pubky.app/bookmarks/:bookmark_id -/// -/// Example URI: -/// -/// `/pub/pubky.app/bookmarks/AF7KQ6NEV5XV1EG5DVJ2E74JJ4` -/// -/// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) -#[derive(Serialize, Deserialize, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppBookmark { - pub uri: String, - pub created_at: i64, -} - -impl PubkyAppBookmark { - /// Creates a new `PubkyAppBookmark` instance. - pub fn new(uri: String) -> Self { - let created_at = timestamp(); - Self { uri, created_at }.sanitize() - } -} - -impl HashId for PubkyAppBookmark { - /// Bookmark ID is created based on the hash of the URI bookmarked - fn get_id_data(&self) -> String { - self.uri.clone() - } -} - -impl HasPath for PubkyAppBookmark { - fn create_path(&self) -> String { - format!("{}bookmarks/{}", APP_PATH, self.create_id()) - } -} - -impl Validatable for PubkyAppBookmark { - fn validate(&self, id: &str) -> Result<(), String> { - self.validate_id(id)?; - // TODO: more bookmarks validation? - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_create_bookmark_id() { - let bookmark = PubkyAppBookmark { - uri: "user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - }; - - let bookmark_id = bookmark.create_id(); - assert_eq!(bookmark_id, "AF7KQ6NEV5XV1EG5DVJ2E74JJ4"); - } - - #[test] - fn test_create_path() { - let bookmark = PubkyAppBookmark { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - }; - let expected_id = bookmark.create_id(); - let expected_path = format!("{}bookmarks/{}", APP_PATH, expected_id); - let path = bookmark.create_path(); - assert_eq!(path, expected_path); - } - - #[test] - fn test_validate_valid() { - let bookmark = - PubkyAppBookmark::new("pubky://user_id/pub/pubky.app/posts/post_id".to_string()); - let id = bookmark.create_id(); - let result = bookmark.validate(&id); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_id() { - let bookmark = PubkyAppBookmark::new("user_id/pub/pubky.app/posts/post_id".to_string()); - let invalid_id = "INVALIDID"; - let result = bookmark.validate(invalid_id); - assert!(result.is_err()); - } - - #[test] - fn test_try_from_valid() { - let bookmark_json = r#" - { - "uri": "user_id/pub/pubky.app/posts/post_id", - "created_at": 1627849723 - } - "#; - - let uri = "user_id/pub/pubky.app/posts/post_id".to_string(); - let bookmark = PubkyAppBookmark::new(uri.clone()); - let id = bookmark.create_id(); - - let blob = bookmark_json.as_bytes(); - let bookmark_parsed = ::try_from(blob, &id).unwrap(); - - assert_eq!(bookmark_parsed.uri, uri); - } -} -``` -./src/models/post.rs -``` -use crate::{ - traits::{HasPath, TimestampId, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use url::Url; - -// Validation -const MAX_SHORT_CONTENT_LENGTH: usize = 1000; -const MAX_LONG_CONTENT_LENGTH: usize = 50000; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents the type of pubky-app posted data -/// Used primarily to best display the content in UI -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] -#[serde(rename_all = "lowercase")] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub enum PubkyAppPostKind { - #[default] - Short, - Long, - Image, - Video, - Link, - File, -} - -impl fmt::Display for PubkyAppPostKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let string_repr = serde_json::to_value(self) - .ok() - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_default(); - write!(f, "{}", string_repr) - } -} - -/// Represents embedded content within a post -#[derive(Serialize, Deserialize, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppPostEmbed { - pub kind: PubkyAppPostKind, // Kind of the embedded content - pub uri: String, // URI of the embedded content -} - -/// Represents raw post in homeserver with content and kind -/// URI: /pub/pubky.app/posts/:post_id -/// Where post_id is CrockfordBase32 encoding of timestamp -/// -/// Example URI: -/// -/// `/pub/pubky.app/posts/00321FCW75ZFY` -#[derive(Serialize, Deserialize, Default, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppPost { - pub content: String, - pub kind: PubkyAppPostKind, - pub parent: Option, // If a reply, the URI of the parent post. - pub embed: Option, - pub attachments: Option>, -} - -impl PubkyAppPost { - /// Creates a new `PubkyAppPost` instance and sanitizes it. - pub fn new( - content: String, - kind: PubkyAppPostKind, - parent: Option, - embed: Option, - attachments: Option>, - ) -> Self { - let post = PubkyAppPost { - content, - kind, - parent, - embed, - attachments, - }; - post.sanitize() - } -} - -impl TimestampId for PubkyAppPost {} - -impl HasPath for PubkyAppPost { - fn create_path(&self) -> String { - format!("{}posts/{}", APP_PATH, self.create_id()) - } -} - -impl Validatable for PubkyAppPost { - fn sanitize(self) -> Self { - // Sanitize content - let mut content = self.content.trim().to_string(); - - // We are using content keyword `[DELETED]` for deleted posts from a homeserver that still have relationships - // placed by other users (replies, tags, etc). This content is exactly matched by the client to apply effects to deleted content. - // Placing posts with content `[DELETED]` is not allowed. - if content == *"[DELETED]" { - content = "empty".to_string() - } - - // Define content length limits based on PubkyAppPostKind - let max_content_length = match self.kind { - PubkyAppPostKind::Short => MAX_SHORT_CONTENT_LENGTH, - PubkyAppPostKind::Long => MAX_LONG_CONTENT_LENGTH, - _ => MAX_SHORT_CONTENT_LENGTH, // Default limit for other kinds - }; - - let content = content.chars().take(max_content_length).collect::(); - - // Sanitize parent URI if present - let parent = if let Some(uri_str) = &self.parent { - match Url::parse(uri_str) { - Ok(url) => Some(url.to_string()), // Valid URI, use normalized version - Err(_) => None, // Invalid URI, discard or handle appropriately - } - } else { - None - }; - - // Sanitize embed if present - let embed = if let Some(embed) = &self.embed { - match Url::parse(&embed.uri) { - Ok(url) => Some(PubkyAppPostEmbed { - kind: embed.kind.clone(), - uri: url.to_string(), // Use normalized version - }), - Err(_) => None, // Invalid URI, discard or handle appropriately - } - } else { - None - }; - - PubkyAppPost { - content, - kind: self.kind, - parent, - embed, - attachments: self.attachments, - } - } - - fn validate(&self, id: &str) -> Result<(), String> { - self.validate_id(id)?; - - // Validate content length - match self.kind { - PubkyAppPostKind::Short => { - if self.content.chars().count() > MAX_SHORT_CONTENT_LENGTH { - return Err( - "Validation Error: Post content exceeds maximum length for Short kind" - .into(), - ); - } - } - PubkyAppPostKind::Long => { - if self.content.chars().count() > MAX_LONG_CONTENT_LENGTH { - return Err( - "Validation Error: Post content exceeds maximum length for Short kind" - .into(), - ); - } - } - _ => (), - }; - - // TODO: additional validation. Attachement URLs...? - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_create_id() { - let post = PubkyAppPost::new( - "Hello World!".to_string(), - PubkyAppPostKind::Short, - None, - None, - None, - ); - - let post_id = post.create_id(); - println!("Generated Post ID: {}", post_id); - - // Assert that the post ID is 13 characters long - assert_eq!(post_id.len(), 13); - } - - #[test] - fn test_new() { - let content = "This is a test post".to_string(); - let kind = PubkyAppPostKind::Short; - let post = PubkyAppPost::new(content.clone(), kind.clone(), None, None, None); - - assert_eq!(post.content, content); - assert_eq!(post.kind, kind); - assert!(post.parent.is_none()); - assert!(post.embed.is_none()); - assert!(post.attachments.is_none()); - } - - #[test] - fn test_create_path() { - let post = PubkyAppPost::new( - "Test post".to_string(), - PubkyAppPostKind::Short, - None, - None, - None, - ); - - let post_id = post.create_id(); - let path = post.create_path(); - - // Check if the path starts with the expected prefix - let prefix = format!("{}posts/", APP_PATH); - assert!(path.starts_with(&prefix)); - - let expected_path_len = prefix.len() + post_id.len(); - assert_eq!(path.len(), expected_path_len); - } - - #[test] - fn test_sanitize() { - let content = " This is a test post with extra whitespace ".to_string(); - let post = PubkyAppPost::new( - content.clone(), - PubkyAppPostKind::Short, - Some("invalid uri".to_string()), - Some(PubkyAppPostEmbed { - kind: PubkyAppPostKind::Link, - uri: "invalid uri".to_string(), - }), - None, - ); - - let sanitized_post = post.sanitize(); - assert_eq!(sanitized_post.content, content.trim()); - assert!(sanitized_post.parent.is_none()); - assert!(sanitized_post.embed.is_none()); - } - - #[test] - fn test_validate_valid() { - let post = PubkyAppPost::new( - "Valid content".to_string(), - PubkyAppPostKind::Short, - None, - None, - None, - ); - - let id = post.create_id(); - let result = post.validate(&id); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_id() { - let post = PubkyAppPost::new( - "Valid content".to_string(), - PubkyAppPostKind::Short, - None, - None, - None, - ); - - let invalid_id = "INVALIDID12345"; - let result = post.validate(invalid_id); - assert!(result.is_err()); - } - - #[test] - fn test_try_from_valid() { - let post_json = r#" - { - "content": "Hello World!", - "kind": "short", - "parent": null, - "embed": null, - "attachments": null - } - "#; - - let id = PubkyAppPost::new( - "Hello World!".to_string(), - PubkyAppPostKind::Short, - None, - None, - None, - ) - .create_id(); - - let blob = post_json.as_bytes(); - let post = ::try_from(blob, &id).unwrap(); - - assert_eq!(post.content, "Hello World!"); - } - - #[test] - fn test_try_from_invalid_content() { - let content = "[DELETED]".to_string(); - let post_json = format!( - r#"{{ - "content": "{}", - "kind": "short", - "parent": null, - "embed": null, - "attachments": null - }}"#, - content - ); - - let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None) - .create_id(); - - let blob = post_json.as_bytes(); - let post = ::try_from(blob, &id).unwrap(); - - assert_eq!(post.content, "empty"); // After sanitization - } -} -``` -./src/models/tag.rs -``` -use crate::{ - common::timestamp, - traits::{HasPath, HashId, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; -use url::Url; - -// Validation -const MAX_TAG_LABEL_LENGTH: usize = 20; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents raw homeserver tag with id -/// URI: /pub/pubky.app/tags/:tag_id -/// -/// Example URI: -/// -/// `/pub/pubky.app/tags/FPB0AM9S93Q3M1GFY1KV09GMQM` -/// -/// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppTag { - pub uri: String, - pub label: String, - pub created_at: i64, -} - -impl PubkyAppTag { - pub fn new(uri: String, label: String) -> Self { - let created_at = timestamp(); - Self { - uri, - label, - created_at, - } - .sanitize() - } -} - -impl HasPath for PubkyAppTag { - fn create_path(&self) -> String { - format!("{}tags/{}", APP_PATH, self.create_id()) - } -} - -impl HashId for PubkyAppTag { - /// Tag ID is created based on the hash of the URI tagged and the label used - fn get_id_data(&self) -> String { - format!("{}:{}", self.uri, self.label) - } -} - -impl Validatable for PubkyAppTag { - fn sanitize(self) -> Self { - // Remove spaces from the tag and keep it as one word - // Returns a lowercase tag - let mut label = self - .label - .chars() - .filter(|c| !c.is_whitespace()) - .collect::() - .to_lowercase(); - - // Enforce maximum label length safely - label = label.chars().take(MAX_TAG_LABEL_LENGTH).collect::(); - - // Sanitize URI - let uri = match Url::parse(&self.uri) { - Ok(url) => { - // If the URL is valid, reformat it to a sanitized string representation - url.to_string() - } - Err(_) => { - // If the URL is invalid, return as-is for error reporting later - self.uri.trim().to_string() - } - }; - - PubkyAppTag { - uri, - label, - created_at: self.created_at, - } - } - - fn validate(&self, id: &str) -> Result<(), String> { - // Validate the tag ID - self.validate_id(id)?; - - // Validate label length - if self.label.chars().count() > MAX_TAG_LABEL_LENGTH { - return Err("Validation Error: Tag label exceeds maximum length".to_string()); - } - - // Validate URI format - match Url::parse(&self.uri) { - Ok(_) => Ok(()), - Err(_) => Err(format!( - "Validation Error: Invalid URI format: {}", - self.uri - )), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{traits::Validatable, APP_PATH}; - - #[test] - fn test_label_id() { - // Precomputed earlier - let tag_id = "CBYS8P6VJPHC5XXT4WDW26662W"; - // Create new tag - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "cool".to_string(), - }; - - let new_tag_id = tag.create_id(); - assert!(!tag_id.is_empty()); - - // Check if the tag ID is correct - assert_eq!(new_tag_id, tag_id); - - let wrong_tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "co0l".to_string(), - }; - - // Assure that the new tag has wrong ID - assert_ne!(wrong_tag.create_id(), tag_id); - } - - #[test] - fn test_create_id() { - let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), - created_at: 1627849723000, - label: "cool".to_string(), - }; - - let tag_id = tag.create_id(); - println!("Generated Tag ID: {}", tag_id); - - // Assert that the tag ID is of expected length - // The length depends on your implementation of create_id - assert!(!tag_id.is_empty()); - } - - #[test] - fn test_new() { - let uri = "https://example.com/post/1".to_string(); - let label = "interesting".to_string(); - let tag = PubkyAppTag::new(uri.clone(), label.clone()); - - assert_eq!(tag.uri, uri); - assert_eq!(tag.label, label); - // Check that created_at is recent - let now = timestamp(); - - assert!(tag.created_at <= now && tag.created_at >= now - 1_000_000); // within 1 second - } - - #[test] - fn test_create_path() { - let tag = PubkyAppTag { - uri: "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032FNCGXE3R0".to_string(), - created_at: 1627849723000, - label: "cool".to_string(), - }; - - let expected_id = tag.create_id(); - let expected_path = format!("{}tags/{}", APP_PATH, expected_id); - let path = tag.create_path(); - - assert_eq!(path, expected_path); - } - - #[test] - fn test_sanitize() { - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), - label: " CoOl ".to_string(), - created_at: 1627849723000, - }; - - let sanitized_tag = tag.sanitize(); - assert_eq!(sanitized_tag.label, "cool"); - } - - #[test] - fn test_validate_valid() { - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), - label: "cool".to_string(), - created_at: 1627849723000, - }; - - let id = tag.create_id(); - let result = tag.validate(&id); - assert!(result.is_ok()); - } - - #[test] - fn test_validate_invalid_label_length() { - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), - label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), - created_at: 1627849723000, - }; - - let id = tag.create_id(); - let result = tag.validate(&id); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Validation Error: Tag label exceeds maximum length" - ); - } - - #[test] - fn test_validate_invalid_id() { - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), - label: "cool".to_string(), - created_at: 1627849723000, - }; - - let invalid_id = "INVALIDID"; - let result = tag.validate(invalid_id); - assert!(result.is_err()); - // You can check the specific error message if necessary - } - - #[test] - fn test_try_from_valid() { - let tag_json = r#" - { - "uri": "pubky://user_pubky_id/pub/pubky.app/profile.json", - "label": "Cool Tag", - "created_at": 1627849723000 - } - "#; - - let id = PubkyAppTag::new( - "pubky://user_pubky_id/pub/pubky.app/profile.json".to_string(), - "Cool Tag".to_string(), - ) - .create_id(); - - let blob = tag_json.as_bytes(); - let tag = ::try_from(blob, &id).unwrap(); - assert_eq!(tag.uri, "pubky://user_pubky_id/pub/pubky.app/profile.json"); - assert_eq!(tag.label, "cooltag"); // After sanitization - } - - #[test] - fn test_try_from_invalid_uri() { - let tag_json = r#" - { - "uri": "invalid_uri", - "label": "Cool Tag", - "created_at": 1627849723000 - } - "#; - - let id = "D2DV4EZDA03Q3KCRMVGMDYZ8C0"; - let blob = tag_json.as_bytes(); - let result = ::try_from(blob, id); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Validation Error: Invalid URI format: invalid_uri" - ); - } - - #[test] - fn test_incorrect_label() { - let tag = PubkyAppTag { - uri: "user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "cool".to_string(), - }; - let tag_id = tag.create_id(); - - if let Err(e) = tag.validate(&tag_id) { - assert_eq!( - e.to_string(), - format!("Validation Error: Invalid URI format: {}", tag.uri), - "The error message is not related URI or the message description is wrong" - ) - }; - - let tag = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "coolc00lcolaca0g00llooll".to_string(), - }; - - // Precomputed earlier - let label_id = tag.create_id(); - - if let Err(e) = tag.validate(&label_id) { - assert_eq!( - e.to_string(), - "Validation Error: Tag label exceeds maximum length".to_string(), - "The error message is not related tag length or the message description is wrong" - ) - }; - } - - #[test] - fn test_white_space_tag() { - // All the tags has to be that label after sanitation - let label = "cool"; - - let leading_whitespace = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: " cool".to_string(), - }; - let mut sanitazed_label = leading_whitespace.sanitize(); - assert_eq!(sanitazed_label.label, label); - - let trailing_whitespace = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: " cool".to_string(), - }; - sanitazed_label = trailing_whitespace.sanitize(); - assert_eq!(sanitazed_label.label, label); - - let space_between = PubkyAppTag { - uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: " co ol ".to_string(), - }; - sanitazed_label = space_between.sanitize(); - assert_eq!(sanitazed_label.label, "cool"); - } -} -``` -./src/models/follow.rs -``` -use crate::{ - common::timestamp, - traits::{HasPubkyIdPath, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents raw homeserver follow object with timestamp -/// -/// On follow objects, the main data is encoded in the path -/// -/// URI: /pub/pubky.app/follows/:user_id -/// -/// Example URI: -/// -/// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` -/// -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppFollow { - pub created_at: i64, -} - -impl PubkyAppFollow { - /// Creates a new `PubkyAppFollow` instance. - pub fn new() -> Self { - let created_at = timestamp(); - Self { created_at } - } -} - -impl Validatable for PubkyAppFollow { - fn validate(&self, _id: &str) -> Result<(), String> { - // TODO: additional follow validation? E.g., validate `created_at`? - Ok(()) - } -} - -impl HasPubkyIdPath for PubkyAppFollow { - fn create_path(&self, pubky_id: &str) -> String { - format!("{}follows/{}", APP_PATH, pubky_id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::Validatable; - - #[test] - fn test_new() { - let follow = PubkyAppFollow::new(); - // Check that created_at is recent - let now = timestamp(); - // within 1 second - assert!(follow.created_at <= now && follow.created_at >= now - 1_000_000); - } - - #[test] - fn test_create_path_with_id() { - let mute = PubkyAppFollow::new(); - let path = mute.create_path("user_id123"); - assert_eq!(path, "/pub/pubky.app/follows/user_id123"); - } - - #[test] - fn test_validate() { - let follow = PubkyAppFollow::new(); - let result = follow.validate("some_user_id"); - assert!(result.is_ok()); - } - - #[test] - fn test_try_from_valid() { - let follow_json = r#" - { - "created_at": 1627849723 - } - "#; - - let blob = follow_json.as_bytes(); - let follow_parsed = - ::try_from(blob, "some_user_id").unwrap(); - - assert_eq!(follow_parsed.created_at, 1627849723); - } -} -``` -./src/models/mute.rs -``` -use crate::{ - common::timestamp, - traits::{HasPubkyIdPath, Validatable}, - APP_PATH, -}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "openapi")] -use utoipa::ToSchema; - -/// Represents raw homeserver Mute object with timestamp -/// URI: /pub/pubky.app/mutes/:user_id -/// -/// Example URI: -/// -/// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` -/// -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(ToSchema))] -pub struct PubkyAppMute { - pub created_at: i64, -} - -impl PubkyAppMute { - /// Creates a new `PubkyAppMute` instance. - pub fn new() -> Self { - let created_at = timestamp(); - Self { created_at } - } -} - -impl Validatable for PubkyAppMute { - fn validate(&self, _id: &str) -> Result<(), String> { - // TODO: additional Mute validation? E.g., validate `created_at` ? - Ok(()) - } -} - -impl HasPubkyIdPath for PubkyAppMute { - fn create_path(&self, pubky_id: &str) -> String { - format!("{}mutes/{}", APP_PATH, pubky_id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::common::timestamp; - use crate::traits::Validatable; - - #[test] - fn test_new() { - let mute = PubkyAppMute::new(); - // Check that created_at is recent - let now = timestamp(); - assert!(mute.created_at <= now && mute.created_at >= now - 1_000_000); - // within 1 second - } - - #[test] - fn test_create_path_with_id() { - let mute = PubkyAppMute::new(); - let path = mute.create_path("user_id123"); - assert_eq!(path, "/pub/pubky.app/mutes/user_id123"); - } - - #[test] - fn test_validate() { - let mute = PubkyAppMute::new(); - let result = mute.validate("some_user_id"); - assert!(result.is_ok()); - } - - #[test] - fn test_try_from_valid() { - let mute_json = r#" - { - "created_at": 1627849723 - } - "#; - - let blob = mute_json.as_bytes(); - let mute_parsed = ::try_from(blob, "some_user_id").unwrap(); - - assert_eq!(mute_parsed.created_at, 1627849723); - } -} -``` -./tests/web.rs -``` -#![cfg(target_arch = "wasm32")] - -extern crate wasm_bindgen_test; -use wasm_bindgen::JsValue; -use wasm_bindgen_test::*; - -use js_sys::{Object, Reflect}; -use pubky_app_specs::{ - create_pubky_app_follow, create_pubky_app_user, traits::Validatable, PubkyAppUser, -}; - -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn test_create_pubky_app_follow() { - let pubky_id = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo"; - - // Call the wasm-exported function - let result = create_pubky_app_follow(pubky_id.into()).unwrap(); - - // The returned `JsValue` is the JSON object { json, id, path } - // We can convert it to a JS object so we can extract fields - let result_obj = Object::try_from(&result).expect("expected a JS object"); - - // For example, check the `path` field - let path_val = - js_sys::Reflect::get(&result_obj, &JsValue::from_str("path")).expect("no path field"); - let path_str = path_val.as_string().expect("path must be a string"); - - assert_eq!( - path_str, - format!("/pub/pubky.app/follows/{}", pubky_id), - "create_pubky_app_follow did not produce expected path" - ); -} - -#[wasm_bindgen_test] -fn test_create_pubky_app_user_ffi() { - let name = String::from("alice"); - let bio = Some(String::from("Alice is a test user.")); - let image = None; - let links = JsValue::NULL; - let status = Some(String::from("testing")); - - let result = create_pubky_app_user(name, bio, image, links, status) - .expect("create_pubky_app_user should not fail"); - - let result_obj = js_sys::Object::try_from(&result).expect("should be an object"); - - let id_val = - js_sys::Reflect::get(&result_obj, &JsValue::from_str("id")).expect("no `id` field"); - let path_val = - js_sys::Reflect::get(&result_obj, &JsValue::from_str("path")).expect("no `path` field"); - // Check the `json` field explicitly - let json_val = Reflect::get(&result_obj, &JsValue::from_str("json")).expect("no `json` field"); - - // Attempt to convert `json_val` to an Object - let json_obj = Object::try_from(&json_val).expect("json field should be an object"); - - let id_str = id_val.as_string().expect("id must be a string"); - let path_str = path_val.as_string().expect("path must be a string"); - - assert_eq!(id_str, "", "Expected ID to be empty"); - assert_eq!(path_str, "/pub/pubky.app/profile.json", "Path is incorrect"); - - // name should be "alice" now: - let username_val = - Reflect::get(&json_obj, &JsValue::from_str("name")).expect("no `name` in json"); - let username_str = username_val.as_string().unwrap(); - assert_eq!(username_str, "alice", "username mismatch"); - - let bio_val = Reflect::get(&json_obj, &JsValue::from_str("bio")).expect("no `bio` in json"); - let bio_str = bio_val.as_string().unwrap(); - assert_eq!(bio_str, "Alice is a test user.", "bio mismatch"); -} - -#[wasm_bindgen_test] -fn test_create_pubky_app_user_oop() { - // Instead of calling `create_pubky_app_user(...)`, we now - // directly construct and validate a `PubkyAppUser`. - - let user = PubkyAppUser::new( - "alice".to_string(), - Some("Alice is a test user.".to_string()), - None, // image - None, // links - Some("testing".to_string()), - ); - - // Perform validation (no ID-based checks for profile.json) - user.validate("").expect("User validation should succeed"); - - // `get_data()` returns a `JsValue` that contains { id, path, json } - let data_value = user.get_data().expect("Should return data"); - - // Convert that JsValue into a JS object so we can pick out fields - let data_obj = Object::try_from(&data_value).expect("expected a JS object"); - - let id_val = Reflect::get(&data_obj, &JsValue::from_str("id")).expect("no `id` field"); - let path_val = Reflect::get(&data_obj, &JsValue::from_str("path")).expect("no `path` field"); - let json_val = Reflect::get(&data_obj, &JsValue::from_str("json")).expect("no `json` field"); - - // Convert to Rust-friendly types - - assert_eq!(id_val.as_string(), None); - - let path_str = path_val.as_string().expect("path must be a string"); - assert_eq!(path_str, "/pub/pubky.app/profile.json", "Path is incorrect"); - - // Inside `json`, we should see `{"name":"alice","bio":"Alice is a test user.",...}` - let json_obj = Object::try_from(&json_val).expect("json field should be an object"); - - let name_val = Reflect::get(&json_obj, &JsValue::from_str("name")).expect("no `name` in json"); - let name_str = name_val.as_string().unwrap_or_default(); - assert_eq!(name_str, "alice", "name mismatch"); - - let bio_val = Reflect::get(&json_obj, &JsValue::from_str("bio")).expect("no `bio` in json"); - let bio_str = bio_val.as_string().unwrap_or_default(); - assert_eq!(bio_str, "Alice is a test user.", "bio mismatch"); - - web_sys::console::log_1(&json_obj); - - let status_val = - Reflect::get(&json_obj, &JsValue::from_str("status")).expect("no `status` in json"); - let status_str = status_val.as_string().unwrap_or_default(); - assert_eq!(status_str, "testing", "status mismatch"); -} -``` -./README.md -``` -# Pubky.app Data Model Specification - -_Version 0.3.0_ - -> ⚠️ **Warning: Rapid Development Phase** -> This specification is in an **early development phase** and is evolving quickly. Expect frequent changes and updates as the system matures. Consider this a **v0 draft**. -> -> When we reach the first stable, long-term support version of the schemas, paths will adopt the format: `pubky.app/v1/` to indicate compatibility and stability. - -### JS package - -Build with - -```bash -wasm-pack build --target bundler -``` - -Test with - -```bash -wasm-pack test --headless --firefox -``` - -## Table of Contents - -1. [Introduction](#introduction) -2. [Quick Start](#quick-start) -3. [Data Models](#data-models) - - [PubkyAppUser](#pubkyappuser) - - [PubkyAppFile](#pubkyappfile) - - [PubkyAppPost](#pubkyapppost) - - [PubkyAppTag](#pubkyapptag) - - [PubkyAppBookmark](#pubkyappbookmark) - - [PubkyAppFollow](#pubkyappfollow) - - [PubkyAppMute](#pubkyappmute) - - [PubkyAppFeed](#pubkyappfeed) - - [PubkyAppLastRead](#pubkyapplastread) -4. [Validation Rules](#validation-rules) - - [Common Rules](#common-rules) - - [ID Generation](#id-generation) -5. [Glossary](#glossary) -6. [Examples](#examples) - - [PubkyAppUser](#example-pubkyappuser) - - [PubkyAppPost](#example-pubkyapppost) - - [PubkyAppTag](#example-pubkyapptag) -7. [License](#license) - ---- - -## Introduction - -This document specifies the data models and validation rules for the **Pubky.app** clients interactions. It defines the structure of data entities, their properties, and the validation rules to ensure data integrity and consistency. This is intended for developers building compatible libraries or clients. - -This document intents to be a faithful representation of our [Rust pubky.app models](https://github.com/pubky/pubky-app-specs/tree/main/src). If you intend to develop in Rust, use them directly. In case of disagreement between this document and the Rust implementation, the Rust implementation prevails. - ---- - -## Quick Start - -Pubky.app models are designed for decentralized content sharing. The system uses a combination of timestamp-based IDs and Blake3-hashed IDs encoded in Crockford Base32 to ensure unique identifiers for each entity. - -### Concepts: - -- **Timestamp IDs** for sequential objects like posts and files. -- **Hash IDs** for content-based uniqueness (e.g., tags and bookmarks). -- **Validation Rules** ensure consistent and interoperable data formats. - ---- - -## Data Models - -### PubkyAppUser - -**Description:** Represents a user's profile information. - -**URI:** `/pub/pubky.app/profile.json` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| --------- | -------- | --------------------------------------- | -------------------------------------------------------------------------------------------- | -| `name` | String | User's name. | Required. Length: 3–50 characters. Cannot be `"[DELETED]"`. | -| `bio` | String | Short biography. | Optional. Maximum length: 160 characters. | -| `image` | String | URL to the user's profile image. | Optional. Valid URL. Maximum length: 300 characters. | -| `links` | Array | List of associated links (title + URL). | Optional. Maximum of 5 links, each with title (100 chars max) and valid URL (300 chars max). | -| `status` | String | User's current status. | Optional. Maximum length: 50 characters. | - -**Validation Notes:** - -- Reserved keyword `[DELETED]` cannot be used for `name`. -- Each `UserLink` in `links` must have a valid title and URL. - -**Example: Valid User** - -```json -{ - "name": "Alice", - "bio": "Toxic maximalist.", - "image": "pubky://user_id/pub/pubky.app/files/0000000000000", - "links": [ - { - "title": "GitHub", - "url": "https://github.com/alice" - } - ], - "status": "Exploring decentralized tech." -} -``` - ---- - -### PubkyAppFile - -**Description:** Represents a file uploaded by the user, containing its metadata, including a reference to the actual blob of the file in `src` property. - -**URI:** `/pub/pubky.app/files/:file_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| -------------- | -------- | --------------------------- | ---------------------------------------------- | -| `name` | String | Name of the file. | Required. Must be 1-255 characters | -| `created_at` | Integer | Unix timestamp of creation. | Required. | -| `src` | String | File blob URL | Required. must be a valid URL. Max length 1024 | -| `content_type` | String | MIME type of the file. | Required. Valid IANA mime types | -| `size` | Integer | Size of the file in bytes. | Required. Positive integer. Max size is 10Mb | - -**Validation Notes:** - -- The `file_id` in the URI must be a valid **Timestamp ID**. - ---- - -### PubkyAppPost - -**Description:** Represents a user's post. - -**URI:** `/pub/pubky.app/posts/:post_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| ------------- | -------- | ------------------------------------ | -------------------------------------------------------------------------- | -| `content` | String | Content of the post. | Required. Max length: 1000 (short), 50000 (long). Cannot be `"[DELETED]"`. | -| `kind` | String | Type of post. | Required. Must be a valid `PubkyAppPostKind` value. | -| `parent` | String | URI of the parent post (if a reply). | Optional. Must be a valid URI if present. | -| `embed` | Object | Embedded content (type + URI). | Optional. URI must be valid if present. | -| `attachments` | Array | List of attachment URIs. | Optional. Each must be a valid URI. | - -**Post Kinds:** - -- `short` -- `long` -- `image` -- `video` -- `link` -- `file` - -**Example: Valid Post** - -```json -{ - "content": "Hello world! This is my first post.", - "kind": "short", - "parent": null, - "embed": { - "kind": "short", - "uri": "pubky://user_id/pub/pubky.app/posts/0000000000000" - }, - "attachments": ["pubky://user_id/pub/pubky.app/files/0000000000000"] -} -``` - ---- - -### PubkyAppTag - -**Description:** Represents a tag applied to a URI. - -**URI:** `/pub/pubky.app/tags/:tag_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| ------------ | -------- | --------------------------- | -------------------------------------------------------- | -| `uri` | String | URI of the tagged object. | Required. Must be a valid URI. | -| `label` | String | Label for the tag. | Required. Trimmed, lowercase. Max length: 20 characters. | -| `created_at` | Integer | Unix timestamp of creation. | Required. | - -**Validation Notes:** - -- The `tag_id` is a **Hash ID** derived from the `uri` and `label`. - ---- - -### PubkyAppBookmark - -**Description:** Represents a bookmark to a URI. - -**URI:** `/pub/pubky.app/bookmarks/:bookmark_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| ------------ | -------- | ---------------------- | ------------------------------ | -| `uri` | String | URI of the bookmark. | Required. Must be a valid URI. | -| `created_at` | Integer | Timestamp of creation. | Required. | - -**Validation Notes:** - -- The `bookmark_id` is a **Hash ID** derived from the `uri`. - ---- - -### PubkyAppFollow - -**Description:** Represents a follow relationship. - -**URI:** `/pub/pubky.app/follows/:user_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| ------------ | -------- | ---------------------- | -------------------- | -| `created_at` | Integer | Timestamp of creation. | Required. | - ---- - -### PubkyAppFeed - -**Description:** Represents a feed configuration. - -**URI:** `/pub/pubky.app/feeds/:feed_id` - -| **Field** | **Type** | **Description** | **Validation Rules** | -| --------- | -------- | ----------------------------------------- | ---------------------------------- | -| `tags` | Array | List of tags for filtering. | Optional. Strings must be trimmed. | -| `reach` | String | Feed visibility (e.g., `all`, `friends`). | Required. Must be a valid reach. | -| `layout` | String | Feed layout style (e.g., `columns`). | Required. Must be valid layout. | -| `sort` | String | Sort order (e.g., `recent`). | Required. Must be valid sort. | -| `content` | String | Type of content filtered. | Optional. | -| `name` | String | Name of the feed. | Required. | - ---- - -## Validation Rules - -### Common Rules - -1. **Timestamp IDs:** 13-character Crockford Base32 strings derived from timestamps (in microseconds). -2. **Hash IDs:** First half of the bytes from the resulting Blake3-hashed strings encoded in Crockford Base32. -3. **URLs:** All URLs must pass standard validation. - ---- - -## License - -This specification is released under the MIT License. -``` -./Cargo.toml -``` -[package] -name = "pubky-app-specs" -version = "0.3.0" -edition = "2021" -description = "Pubky.app Data Model Specifications" -homepage = "https://pubky.app" -repository = "https://github.com/pubky/pubky-app-specs" -license = "MIT" -documentation = "https://github.com/pubky/pubky-app-specs" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.137" -url = "2.5.4" -base32 = "0.5.1" -blake3 = "1.5.5" -mime = "0.3" -utoipa = { git = "https://github.com/juhaku/utoipa", rev = "d522f744259dc4fde5f45d187983fb68c8167029", optional = true } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.100" -serde-wasm-bindgen = "0.6.5" -js-sys = "0.3.77" -web-sys = "0.3.77" - -[dev-dependencies] -pubky-common = "0.1.0" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.50" - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -pubky = "0.3.0" -tokio = { version = "1.43.0", features = ["full"] } -anyhow = "1.0.95" - -[features] -openapi = ["utoipa"] - -[profile.release] -opt-level = "z" # Requests maximum optimization for binary size (“z” stands for “size”), rather than speed. -lto = true # Enables link-time optimization, allowing the compiler to do more aggressive inlining/dead-code elimination across crates. -codegen-units = 1 # Forces compilation into a single code-generation unit, further helping inlining and dead-code elimination at link time. -panic = "abort" # Ensures panics do not generate extra stack-unwinding code, reducing binary bloat. Panics will simply abort execution. -``` diff --git a/src/models/file.rs b/src/models/file.rs index 63ae4e6..ecc1922 100644 --- a/src/models/file.rs +++ b/src/models/file.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::{ common::timestamp, traits::{HasPath, TimestampId, Validatable}, @@ -7,8 +5,14 @@ use crate::{ }; use mime::Mime; use serde::{Deserialize, Serialize}; - +use std::str::FromStr; use url::Url; + +#[cfg(target_arch = "wasm32")] +use crate::traits::ToJson; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + #[cfg(feature = "openapi")] use utoipa::ToSchema; @@ -43,16 +47,45 @@ const VALID_MIME_TYPES: &[&str] = &[ /// Represents a file uploaded by the user. /// URI: /pub/pubky.app/files/:file_id +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Deserialize, Serialize, Debug, Default, Clone)] #[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppFile { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub name: String, pub created_at: i64, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub src: String, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub content_type: String, pub size: i64, } +#[cfg(target_arch = "wasm32")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl PubkyAppFile { + // Getters clone the data out because String/JsValue is not Copy. + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn name(&self) -> String { + self.name.clone() + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn src(&self) -> String { + self.src.clone() + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn content_type(&self) -> String { + self.content_type.clone() + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = toJson))] + pub fn json(&self) -> Result { + self.to_json() + } +} + +#[cfg(target_arch = "wasm32")] +impl ToJson for PubkyAppFile {} + impl PubkyAppFile { /// Creates a new `PubkyAppFile` instance. pub fn new(name: String, src: String, content_type: String, size: i64) -> Self { diff --git a/src/models/follow.rs b/src/models/follow.rs index f203f98..5922c5f 100644 --- a/src/models/follow.rs +++ b/src/models/follow.rs @@ -9,8 +9,6 @@ use serde::{Deserialize, Serialize}; use crate::traits::ToJson; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsValue; #[cfg(feature = "openapi")] use utoipa::ToSchema; diff --git a/src/models/mute.rs b/src/models/mute.rs index 9ea90c2..a26c099 100644 --- a/src/models/mute.rs +++ b/src/models/mute.rs @@ -5,6 +5,11 @@ use crate::{ }; use serde::{Deserialize, Serialize}; +#[cfg(target_arch = "wasm32")] +use crate::traits::ToJson; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + #[cfg(feature = "openapi")] use utoipa::ToSchema; @@ -15,6 +20,7 @@ use utoipa::ToSchema; /// /// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` /// +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Serialize, Deserialize, Default, Debug, Clone)] #[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppMute { @@ -29,6 +35,18 @@ impl PubkyAppMute { } } +#[cfg(target_arch = "wasm32")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl PubkyAppMute { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = toJson))] + pub fn json(&self) -> Result { + self.to_json() + } +} + +#[cfg(target_arch = "wasm32")] +impl ToJson for PubkyAppMute {} + impl Validatable for PubkyAppMute { fn validate(&self, _id: &str) -> Result<(), String> { // TODO: additional Mute validation? E.g., validate `created_at` ? diff --git a/src/models/post.rs b/src/models/post.rs index 99c564e..f0ccf08 100644 --- a/src/models/post.rs +++ b/src/models/post.rs @@ -10,12 +10,18 @@ use url::Url; const MAX_SHORT_CONTENT_LENGTH: usize = 1000; const MAX_LONG_CONTENT_LENGTH: usize = 50000; +#[cfg(target_arch = "wasm32")] +use crate::traits::ToJson; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + #[cfg(feature = "openapi")] use utoipa::ToSchema; /// Represents the type of pubky-app posted data /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[serde(rename_all = "lowercase")] #[cfg_attr(feature = "openapi", derive(ToSchema))] pub enum PubkyAppPostKind { @@ -39,11 +45,14 @@ impl fmt::Display for PubkyAppPostKind { } /// Represents embedded content within a post +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Serialize, Deserialize, Default, Clone)] #[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppPostEmbed { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub kind: PubkyAppPostKind, // Kind of the embedded content - pub uri: String, // URI of the embedded content + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub uri: String, // URI of the embedded content } /// Represents raw post in homeserver with content and kind @@ -53,18 +62,63 @@ pub struct PubkyAppPostEmbed { /// Example URI: /// /// `/pub/pubky.app/posts/00321FCW75ZFY` +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Serialize, Deserialize, Default, Clone)] #[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppPost { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub content: String, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub kind: PubkyAppPostKind, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub parent: Option, // If a reply, the URI of the parent post. + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub embed: Option, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] pub attachments: Option>, } +#[cfg(target_arch = "wasm32")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl PubkyAppPost { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn content(&self) -> String { + self.content.clone() + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn kind(&self) -> PubkyAppPostKind { + self.kind.clone() + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn parent(&self) -> Option { + self.parent.clone() + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn embed(&self) -> Option { + self.embed.clone() + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter))] + pub fn attachments(&self) -> Option> { + self.attachments.clone() + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = toJson))] + pub fn json(&self) -> Result { + self.to_json() + } +} + +#[cfg(target_arch = "wasm32")] +impl ToJson for PubkyAppPost {} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] impl PubkyAppPost { /// Creates a new `PubkyAppPost` instance and sanitizes it. + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] pub fn new( content: String, kind: PubkyAppPostKind, diff --git a/src/models/user.rs b/src/models/user.rs index 0a0094f..f81d414 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -9,8 +9,6 @@ use url::Url; use crate::traits::ToJson; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsValue; #[cfg(feature = "openapi")] use utoipa::ToSchema; diff --git a/src/wasm.rs b/src/wasm.rs index 74fc154..0a86c28 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -42,7 +42,7 @@ impl Meta { } impl Meta { - /// Internal helper. Generates JSON from `object` + sets `id`, `path`, and `url`. + /// Internal helper. Generates meta's `id`, `path`, and `url`. pub fn from_object(object_id: String, pubky_id: String, path: String) -> Self { Self { id: object_id, @@ -54,69 +54,88 @@ impl Meta { /// Represents a user's single link with a title and URL. #[wasm_bindgen] -pub struct PubkyAppBuilder { +pub struct PubkySpecsBuilder { #[wasm_bindgen(skip)] pub pubky_id: String, } -#[wasm_bindgen] -pub struct FollowResult { - follow: PubkyAppFollow, - meta: Meta, -} - -#[wasm_bindgen] -impl FollowResult { - // Getters clone the data out because String/JsValue is not Copy. - #[wasm_bindgen(getter)] - pub fn meta(&self) -> Meta { - self.meta.clone() - } - - #[wasm_bindgen(getter)] - pub fn follow(&self) -> PubkyAppFollow { - self.follow.clone() - } -} +/// A macro to generate result structs and `wasm_bindgen`-exposed getters. +/// A struct for each `create_*()` function is needed if we want +/// correct TS types +/// +/// This macro creates a struct with the specified name (`$struct_name`), +/// containing: +/// - A primary field (`$field_name`) of type `$field_type`. +/// - A `meta` field of type `Meta`. +/// +/// It also generates getters for both fields. +/// +/// # Usage +/// ```rust +/// result_struct!(PostResult, post, PubkyAppPost); +/// ``` +/// Expands to: +/// ```rust +/// #[wasm_bindgen] +/// pub struct PostResult { +/// post: PubkyAppPost, +/// meta: Meta, +/// } +/// +/// #[wasm_bindgen] +/// impl PostResult { +/// #[wasm_bindgen(getter)] +/// pub fn post(&self) -> PubkyAppPost { self.post.clone() } +/// +/// #[wasm_bindgen(getter)] +/// pub fn meta(&self) -> Meta { self.meta.clone() } +/// } +/// ``` +macro_rules! result_struct { + ($struct_name:ident, $field_name:ident, $field_type:ty) => { + #[wasm_bindgen] + pub struct $struct_name { + $field_name: $field_type, + meta: Meta, + } -#[wasm_bindgen] -pub struct UserResult { - user: PubkyAppUser, - meta: Meta, + #[wasm_bindgen] + impl $struct_name { + #[wasm_bindgen(getter)] + pub fn $field_name(&self) -> $field_type { + self.$field_name.clone() + } + + #[wasm_bindgen(getter)] + pub fn meta(&self) -> Meta { + self.meta.clone() + } + } + }; } -#[wasm_bindgen] -impl UserResult { - // Expose read-only getters for TS: - #[wasm_bindgen(getter)] - pub fn user(&self) -> PubkyAppUser { - self.user.clone() - } - #[wasm_bindgen(getter)] - pub fn meta(&self) -> Meta { - self.meta.clone() - } -} +result_struct!(UserResult, user, PubkyAppUser); +result_struct!(FileResult, file, PubkyAppFile); +result_struct!(FollowResult, follow, PubkyAppFollow); +result_struct!(PostResult, post, PubkyAppPost); +// result_struct!(FeedResult, feed, PubkyAppFeed); +// result_struct!(TagResult, tag, PubkyAppTag); +// result_struct!(BookmarkResult, bookmark, PubkyAppBookmark); +result_struct!(MuteResult, mute, PubkyAppMute); +// result_struct!(LastReadResult, last_read, PubkyAppLastRead); +// result_struct!(BlobResult, blob, PubkyAppBlob); #[wasm_bindgen] -impl PubkyAppBuilder { +impl PubkySpecsBuilder { /// Creates a new `PubkyAppBuilder` instance. #[wasm_bindgen(constructor)] pub fn new(pubky_id: String) -> Self { Self { pubky_id } } - #[wasm_bindgen(js_name = createFollow)] - pub fn create_follow(&self, followee_id: String) -> Result { - let follow = PubkyAppFollow::new(); - follow.validate(&followee_id)?; // No ID in follow, so we pass user ID or empty - - // Path requires the user ID - let path = follow.create_path(&followee_id); - let meta = Meta::from_object("".to_string(), self.pubky_id.clone(), path); - // Return an empty ID for follow - Ok(FollowResult { follow, meta }) - } + // // ----------------------------------------------------------------------------- + // // 1. PubkyAppUser + // // ----------------------------------------------------------------------------- #[wasm_bindgen(js_name = createUser)] pub fn create_user( @@ -145,273 +164,242 @@ impl PubkyAppBuilder { // 4) Return a typed struct containing both Ok(UserResult { user, meta }) } -} -// // ----------------------------------------------------------------------------- -// // 1. PubkyAppUser -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_user( -// name: String, -// bio: Option, -// image: Option, -// links: JsValue, // JSON array of PubkyAppUserLink -// status: Option, -// ) -> Result { -// // Convert links to Option> -// let links_vec: Option> = if links.is_null() || links.is_undefined() { -// None -// } else { -// from_value(links)? -// }; - -// // Create user, sanitize, then validate -// let user = PubkyAppUser::new(name, bio, image, links_vec, status); -// user.validate("")?; // no ID-based validation - -// // We have no ID for PubkyAppUser. The path is always profile.json -// let path = user.create_path(); - -// build_create_result(&user, "", &path) -// } - -// // ----------------------------------------------------------------------------- -// // 2. PubkyAppFeed -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_feed( -// tags: JsValue, // JSON array of strings -// reach: String, -// layout: String, -// sort: String, -// content: Option, -// name: String, -// ) -> Result { -// // Convert tags -// let tags_vec: Option> = if tags.is_null() || tags.is_undefined() { -// None -// } else { -// from_value(tags)? -// }; - -// // Convert feed reach -// let reach_enum = match reach.as_str() { -// "following" => PubkyAppFeedReach::Following, -// "followers" => PubkyAppFeedReach::Followers, -// "friends" => PubkyAppFeedReach::Friends, -// "all" => PubkyAppFeedReach::All, -// _ => return Err(JsValue::from_str("Invalid feed reach")), -// }; - -// // Convert layout -// let layout_enum = match layout.as_str() { -// "columns" => PubkyAppFeedLayout::Columns, -// "wide" => PubkyAppFeedLayout::Wide, -// "visual" => PubkyAppFeedLayout::Visual, -// _ => return Err(JsValue::from_str("Invalid feed layout")), -// }; - -// // Convert sort -// let sort_enum = match sort.as_str() { -// "recent" => PubkyAppFeedSort::Recent, -// "popularity" => PubkyAppFeedSort::Popularity, -// _ => return Err(JsValue::from_str("Invalid feed sort")), -// }; - -// // Convert content kind -// let content_kind = match content.as_deref() { -// Some("short") => Some(PubkyAppPostKind::Short), -// Some("long") => Some(PubkyAppPostKind::Long), -// Some("image") => Some(PubkyAppPostKind::Image), -// Some("video") => Some(PubkyAppPostKind::Video), -// Some("link") => Some(PubkyAppPostKind::Link), -// Some("file") => Some(PubkyAppPostKind::File), -// None => None, -// Some(_) => return Err(JsValue::from_str("Invalid content kind")), -// }; - -// // Build feed, sanitize, validate -// let feed = PubkyAppFeed::new( -// tags_vec, -// reach_enum, -// layout_enum, -// sort_enum, -// content_kind, -// name, -// ); -// let feed_id = feed.create_id(); -// feed.validate(&feed_id)?; - -// let path = feed.create_path(); -// build_create_result(&feed, &feed_id, &path) -// } - -// // ----------------------------------------------------------------------------- -// // 3. PubkyAppFile -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_file( -// name: String, -// src: String, -// content_type: String, -// size: i64, -// ) -> Result { -// let file = PubkyAppFile::new(name, src, content_type, size); -// let file_id = file.create_id(); -// file.validate(&file_id)?; - -// let path = file.create_path(); -// build_create_result(&file, &file_id, &path) -// } - -// // ----------------------------------------------------------------------------- -// // 4. PubkyAppPost -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_post( -// content: String, -// kind: String, -// parent: Option, -// embed: JsValue, // JSON object { kind: string, uri: string } or null -// attachments: JsValue, // JSON array of string or null -// ) -> Result { -// // Convert kind -// let kind_enum = match kind.as_str() { -// "short" => PubkyAppPostKind::Short, -// "long" => PubkyAppPostKind::Long, -// "image" => PubkyAppPostKind::Image, -// "video" => PubkyAppPostKind::Video, -// "link" => PubkyAppPostKind::Link, -// "file" => PubkyAppPostKind::File, -// _ => return Err(JsValue::from_str("Invalid post kind")), -// }; - -// // Convert embed -// let embed_option: Option = if embed.is_null() || embed.is_undefined() { -// None -// } else { -// from_value(embed)? -// }; - -// // Convert attachments -// let attachments_vec: Option> = -// if attachments.is_null() || attachments.is_undefined() { -// None -// } else { -// from_value(attachments)? -// }; - -// // Build the post, sanitize, validate -// let post = PubkyAppPost::new(content, kind_enum, parent, embed_option, attachments_vec); -// let post_id = post.create_id(); -// post.validate(&post_id)?; - -// let path = post.create_path(); -// build_create_result(&post, &post_id, &path) -// } - -// // ----------------------------------------------------------------------------- -// // 5. PubkyAppTag -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_tag(uri: String, label: String) -> Result { -// let tag = PubkyAppTag::new(uri, label); -// let tag_id = tag.create_id(); -// tag.validate(&tag_id)?; - -// let path = tag.create_path(); -// build_create_result(&tag, &tag_id, &path) -// } - -// // ----------------------------------------------------------------------------- -// // 6. PubkyAppBookmark -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_bookmark(uri: String) -> Result { -// let bookmark = PubkyAppBookmark::new(uri); -// let bookmark_id = bookmark.create_id(); -// bookmark.validate(&bookmark_id)?; - -// let path = bookmark.create_path(); -// build_create_result(&bookmark, &bookmark_id, &path) -// } - -// // ----------------------------------------------------------------------------- -// // 7. PubkyAppFollow -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_follow(pubky_id: String) -> Result { -// let follow = PubkyAppFollow::new(); -// follow.validate(&pubky_id)?; // No ID in follow, so we pass user ID or empty - -// // Path requires the user ID -// let path = follow.create_path(&pubky_id); - -// // Return an empty ID for follow -// build_create_result(&follow, "", &path) -// } - -// // ----------------------------------------------------------------------------- -// // 8. PubkyAppMute -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_mute(pubky_id: String) -> Result { -// let mute = PubkyAppMute::new(); -// mute.validate(&pubky_id)?; - -// let path = mute.create_path(&pubky_id); -// build_create_result(&mute, "", &path) -// } - -// // ----------------------------------------------------------------------------- -// // 9. PubkyAppLastRead -// // ----------------------------------------------------------------------------- - -// #[wasm_bindgen] -// pub fn create_pubky_app_last_read() -> Result { -// let last_read = PubkyAppLastRead::new(); -// last_read.validate("")?; - -// let path = last_read.create_path(); -// build_create_result(&last_read, "", &path) -// } - -// // ----------------------------------------------------------------------------- -// // 10. PubkyAppBlob -// // ----------------------------------------------------------------------------- - -// /// A small wrapper for JSON-serializing the blob data as base64. -// #[derive(Serialize, Deserialize, Clone)] -// pub struct PubkyAppBlobJson { -// pub data_base64: String, -// } - -// // #[wasm_bindgen] -// // pub fn create_pubky_app_blob(blob_data: JsValue) -> Result { -// // // 1) Convert from JsValue (Uint8Array in JS) -> Vec in Rust -// // let data_vec: Vec = from_value(blob_data) -// // .map_err(|e| JsValue::from_str(&format!("Invalid blob bytes: {}", e)))?; - -// // // 2) Build the PubkyAppBlob -// // let blob = PubkyAppBlob(data_vec); - -// // // 3) Generate ID and path -// // let id = blob.create_id(); -// // let path = blob.create_path(); - -// // // 4) Provide a minimal JSON representation (e.g. base64) -// // let json_blob = PubkyAppBlobJson { -// // data_base64: base64::encode(&blob.0), -// // }; - -// // // 5) Return { json, id, path } -// // build_create_result(&json_blob, &id, &path) -// // } + // ----------------------------------------------------------------------------- + // 2. PubkyAppFeed + // ----------------------------------------------------------------------------- + + // #[wasm_bindgen(js_name = createFeed)] + // pub fn create_feed( + // &self, + // tags: JsValue, + // reach: String, + // layout: String, + // sort: String, + // content: Option, + // name: String, + // ) -> Result { + // let tags_vec: Option> = if tags.is_null() || tags.is_undefined() { + // None + // } else { + // from_value(tags)? + // }; + + // let reach_enum = match reach.as_str() { + // "following" => PubkyAppFeedReach::Following, + // "followers" => PubkyAppFeedReach::Followers, + // "friends" => PubkyAppFeedReach::Friends, + // "all" => PubkyAppFeedReach::All, + // _ => return Err(JsValue::from_str("Invalid feed reach")), + // }; + + // let layout_enum = match layout.as_str() { + // "columns" => PubkyAppFeedLayout::Columns, + // "wide" => PubkyAppFeedLayout::Wide, + // "visual" => PubkyAppFeedLayout::Visual, + // _ => return Err(JsValue::from_str("Invalid feed layout")), + // }; + + // let sort_enum = match sort.as_str() { + // "recent" => PubkyAppFeedSort::Recent, + // "popularity" => PubkyAppFeedSort::Popularity, + // _ => return Err(JsValue::from_str("Invalid feed sort")), + // }; + + // let content_kind = match content.as_deref() { + // Some("short") => Some(PubkyAppPostKind::Short), + // Some("long") => Some(PubkyAppPostKind::Long), + // Some("image") => Some(PubkyAppPostKind::Image), + // Some("video") => Some(PubkyAppPostKind::Video), + // Some("link") => Some(PubkyAppPostKind::Link), + // Some("file") => Some(PubkyAppPostKind::File), + // None => None, + // Some(_) => return Err(JsValue::from_str("Invalid content kind")), + // }; + + // let feed = PubkyAppFeed::new( + // tags_vec, + // reach_enum, + // layout_enum, + // sort_enum, + // content_kind, + // name, + // ); + // let feed_id = feed.create_id(); + // feed.validate(&feed_id)?; + + // let path = feed.create_path(); + // let meta = Meta::from_object(feed_id, self.pubky_id.clone(), path); + + // Ok(FeedResult { feed, meta }) + // } + + // ----------------------------------------------------------------------------- + // 3. PubkyAppFile + // ----------------------------------------------------------------------------- + + #[wasm_bindgen(js_name = createFile)] + pub fn create_file( + &self, + name: String, + src: String, + content_type: String, + size: i64, + ) -> Result { + let file = PubkyAppFile::new(name, src, content_type, size); + let file_id = file.create_id(); + file.validate(&file_id)?; + + let path = file.create_path(); + let meta = Meta::from_object(file_id, self.pubky_id.clone(), path); + + Ok(FileResult { file, meta }) + } + + // ----------------------------------------------------------------------------- + // 4. PubkyAppPost + // ----------------------------------------------------------------------------- + + #[wasm_bindgen(js_name = createPost)] + pub fn create_post( + &self, + content: String, + kind: String, + parent: Option, + embed: JsValue, + attachments: JsValue, + ) -> Result { + let kind_enum = match kind.as_str() { + "short" => PubkyAppPostKind::Short, + "long" => PubkyAppPostKind::Long, + "image" => PubkyAppPostKind::Image, + "video" => PubkyAppPostKind::Video, + "link" => PubkyAppPostKind::Link, + "file" => PubkyAppPostKind::File, + _ => return Err(JsValue::from_str("Invalid post kind")), + }; + + let embed_option: Option = if embed.is_null() || embed.is_undefined() { + None + } else { + from_value(embed)? + }; + + let attachments_vec: Option> = + if attachments.is_null() || attachments.is_undefined() { + None + } else { + from_value(attachments)? + }; + + let post = PubkyAppPost::new(content, kind_enum, parent, embed_option, attachments_vec); + let post_id = post.create_id(); + post.validate(&post_id)?; + + let path = post.create_path(); + let meta = Meta::from_object(post_id, self.pubky_id.clone(), path); + + Ok(PostResult { post, meta }) + } + + // // ----------------------------------------------------------------------------- + // // 5. PubkyAppTag + // // ----------------------------------------------------------------------------- + + // #[wasm_bindgen(js_name = createTag)] + // pub fn create_tag(&self, uri: String, label: String) -> Result { + // let tag = PubkyAppTag::new(uri, label); + // let tag_id = tag.create_id(); + // tag.validate(&tag_id)?; + + // let path = tag.create_path(); + // let meta = Meta::from_object(tag_id, self.pubky_id.clone(), path); + + // Ok(TagResult { tag, meta }) + // } + + // // ----------------------------------------------------------------------------- + // // 6. PubkyAppBookmark + // // ----------------------------------------------------------------------------- + + // #[wasm_bindgen(js_name = createBookmark)] + // pub fn create_bookmark(&self, uri: String) -> Result { + // let bookmark = PubkyAppBookmark::new(uri); + // let bookmark_id = bookmark.create_id(); + // bookmark.validate(&bookmark_id)?; + + // let path = bookmark.create_path(); + // let meta = Meta::from_object(bookmark_id, self.pubky_id.clone(), path); + + // Ok(BookmarkResult { bookmark, meta }) + // } + + // ----------------------------------------------------------------------------- + // 7. PubkyAppFollow + // ----------------------------------------------------------------------------- + + #[wasm_bindgen(js_name = createFollow)] + pub fn create_follow(&self, followee_id: String) -> Result { + let follow = PubkyAppFollow::new(); + follow.validate(&followee_id)?; // No ID in follow, so we pass user ID or empty + + // Path requires the user ID + let path = follow.create_path(&followee_id); + let meta = Meta::from_object(followee_id, self.pubky_id.clone(), path); + + Ok(FollowResult { follow, meta }) + } + + // ----------------------------------------------------------------------------- + // 8. PubkyAppMute + // ----------------------------------------------------------------------------- + + #[wasm_bindgen(js_name = createMute)] + pub fn create_mute(&self, pubky_id: String) -> Result { + let mute = PubkyAppMute::new(); + mute.validate(&pubky_id)?; + + let path = mute.create_path(&pubky_id); + let meta = Meta::from_object(pubky_id, self.pubky_id.clone(), path); + + Ok(MuteResult { mute, meta }) + } + + // // ----------------------------------------------------------------------------- + // // 9. PubkyAppLastRead + // // ----------------------------------------------------------------------------- + + // #[wasm_bindgen(js_name = createLastRead)] + // pub fn create_last_read(&self) -> Result { + // let last_read = PubkyAppLastRead::new(); + // last_read.validate("")?; + + // let path = last_read.create_path(); + // let meta = Meta::from_object("".to_string(), self.pubky_id.clone(), path); + + // Ok(LastReadResult { last_read, meta }) + // } + + // // ----------------------------------------------------------------------------- + // // 10. PubkyAppBlob + // // ----------------------------------------------------------------------------- + + // #[wasm_bindgen(js_name = createBlob)] + // pub fn create_blob(&self, blob_data: JsValue) -> Result { + // // Convert from JsValue (Uint8Array in JS) -> Vec in Rust + // let data_vec: Vec = from_value(blob_data) + // .map_err(|e| JsValue::from_str(&format!("Invalid blob bytes: {}", e)))?; + + // // Create the PubkyAppBlob + // let blob = PubkyAppBlob(data_vec); + + // // Generate ID and path + // let id = blob.create_id(); + // blob.validate(&id)?; + + // let path = blob.create_path(); + // let meta = Meta::from_object(id, self.pubky_id.clone(), path); + + // Ok(BlobResult { blob, meta }) + // } +} diff --git a/tests/web.rs b/tests/web.rs index fd81bc4..c96d0cf 100644 --- a/tests/web.rs +++ b/tests/web.rs @@ -2,7 +2,7 @@ extern crate wasm_bindgen_test; use js_sys::Array; -use pubky_app_specs::{PubkyAppBuilder, PubkyAppUserLink}; +use pubky_app_specs::{PubkyAppUserLink, PubkySpecsBuilder}; use serde_wasm_bindgen::to_value; use wasm_bindgen::JsValue; use wasm_bindgen_test::*; @@ -11,7 +11,7 @@ wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn test_create_follow() { - let specs = PubkyAppBuilder::new("test_pubky_id".to_string()); + let specs = PubkySpecsBuilder::new("test_pubky_id".to_string()); let result = specs .create_follow("followee_123".to_string()) @@ -25,13 +25,13 @@ fn test_create_follow() { meta.url(), "pubky://test_pubky_id/pub/pubky.app/follows/followee_123" ); - assert_eq!(meta.id(), ""); + assert_eq!(meta.id(), "followee_123"); assert!(follow.created_at > 0); } #[wasm_bindgen_test] fn test_create_user_rust_api() { - let specs = PubkyAppBuilder::new("test_pubky_id".to_string()); + let specs = PubkySpecsBuilder::new("test_pubky_id".to_string()); // Prepare links as a JS-compatible array let links = Array::new(); @@ -100,7 +100,7 @@ fn test_create_user_rust_api() { #[wasm_bindgen_test] fn test_create_user_with_minimal_data() { - let specs = PubkyAppBuilder::new("test_pubky_id".to_string()); + let specs = PubkySpecsBuilder::new("test_pubky_id".to_string()); // Call `create_user` with minimal data let result = specs