diff --git a/js/app/package.json b/js/app/package.json index 5febd22b..f8eab8a2 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -39,6 +39,7 @@ "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", "@reduxjs/toolkit": "^2.3.0", + "@sentry/react-native": "^7.0.1", "@streamplace/atproto-oauth-client-react-native": "workspace:*", "@streamplace/components": "workspace:*", "@tanstack/react-query": "^5.59.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5531c6d8..abb426b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: '@reduxjs/toolkit': specifier: ^2.3.0 version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@19.0.0)(redux@5.0.1))(react@19.0.0) + '@sentry/react-native': + specifier: ^7.0.1 + version: 7.0.1(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) '@streamplace/atproto-oauth-client-react-native': specifier: workspace:* version: link:../atproto-oauth-client-react-native @@ -3787,6 +3790,107 @@ packages: '@scure/bip39@1.4.0': resolution: {integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==} + '@sentry-internal/browser-utils@10.8.0': + resolution: {integrity: sha512-FaQX9eefc8sh3h3ZQy16U73KiH0xgDldXnrFiWK6OeWg8X4bJpnYbLqEi96LgHiQhjnnz+UQP1GDzH5oFuu5fA==} + engines: {node: '>=18'} + + '@sentry-internal/feedback@10.8.0': + resolution: {integrity: sha512-n7SqgFQItq4QSPG7bCjcZcIwK6AatKnnmSDJ/i6e8jXNIyLwkEuY2NyvTXACxVdO/kafGD5VmrwnTo3Ekc1AMg==} + engines: {node: '>=18'} + + '@sentry-internal/replay-canvas@10.8.0': + resolution: {integrity: sha512-jC4OOwiNgrlIPeXIPMLkaW53BSS1do+toYHoWzzO5AXGpN6jRhanoSj36FpVuH2N3kFnxKVfVxrwh8L+/3vFWg==} + engines: {node: '>=18'} + + '@sentry-internal/replay@10.8.0': + resolution: {integrity: sha512-9+qDEoEjv4VopLuOzK1zM4LcvcUsvB5N0iJ+FRCM3XzzOCbebJOniXTQbt5HflJc3XLnQNKFdKfTfgj8M/0RKQ==} + engines: {node: '>=18'} + + '@sentry/babel-plugin-component-annotate@4.3.0': + resolution: {integrity: sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==} + engines: {node: '>= 14'} + + '@sentry/browser@10.8.0': + resolution: {integrity: sha512-2J7HST8/ixCaboq17yFn/j/OEokXSXoCBMXRrFx4FKJggKWZ90e2Iau5mP/IPPhrW+W9zCptCgNMY0167wS4qA==} + engines: {node: '>=18'} + + '@sentry/cli-darwin@2.53.0': + resolution: {integrity: sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.53.0': + resolution: {integrity: sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd, android] + + '@sentry/cli-linux-arm@2.53.0': + resolution: {integrity: sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd, android] + + '@sentry/cli-linux-i686@2.53.0': + resolution: {integrity: sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd, android] + + '@sentry/cli-linux-x64@2.53.0': + resolution: {integrity: sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd, android] + + '@sentry/cli-win32-arm64@2.53.0': + resolution: {integrity: sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@sentry/cli-win32-i686@2.53.0': + resolution: {integrity: sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.53.0': + resolution: {integrity: sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.53.0': + resolution: {integrity: sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/core@10.8.0': + resolution: {integrity: sha512-scYzM/UOItu4PjEq6CpHLdArpXjIS0laHYxE4YjkIbYIH6VMcXGQbD/FSBClsnCr1wXRnlXfXBzj0hrQAFyw+Q==} + engines: {node: '>=18'} + + '@sentry/react-native@7.0.1': + resolution: {integrity: sha512-xz8ON51qSDvcHVFkdLo0b7rlrQVXpRVXqzm7e1+nHEZ07TX0o+utxx04akxD1Z4hmGPTWPmsHeMlm7diV9NtTQ==} + hasBin: true + peerDependencies: + expo: '>=49.0.0' + react: '>=17.0.0' + react-native: '>=0.65.0' + peerDependenciesMeta: + expo: + optional: true + + '@sentry/react@10.8.0': + resolution: {integrity: sha512-w/dGLMCLJG2lp8gKVKX1jjeg2inXewKfPb73+PS1CDi9/ihvqZU2DAXxnaNsBA7YYtGwlWVJe1bLAqguwTEpqw==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/types@10.8.0': + resolution: {integrity: sha512-xRe41/KvnNt4o6t5YeB+yBRTWvLUu6FJpft/VBOs4Bfh1/6rz+l78oxSCtpXo3MsfTd5185I0uuggAjEdD4Y6g==} + engines: {node: '>=18'} + '@shikijs/core@3.3.0': resolution: {integrity: sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==} @@ -17159,6 +17263,107 @@ snapshots: '@noble/hashes': 1.5.0 '@scure/base': 1.1.9 + '@sentry-internal/browser-utils@10.8.0': + dependencies: + '@sentry/core': 10.8.0 + + '@sentry-internal/feedback@10.8.0': + dependencies: + '@sentry/core': 10.8.0 + + '@sentry-internal/replay-canvas@10.8.0': + dependencies: + '@sentry-internal/replay': 10.8.0 + '@sentry/core': 10.8.0 + + '@sentry-internal/replay@10.8.0': + dependencies: + '@sentry-internal/browser-utils': 10.8.0 + '@sentry/core': 10.8.0 + + '@sentry/babel-plugin-component-annotate@4.3.0': {} + + '@sentry/browser@10.8.0': + dependencies: + '@sentry-internal/browser-utils': 10.8.0 + '@sentry-internal/feedback': 10.8.0 + '@sentry-internal/replay': 10.8.0 + '@sentry-internal/replay-canvas': 10.8.0 + '@sentry/core': 10.8.0 + + '@sentry/cli-darwin@2.53.0': + optional: true + + '@sentry/cli-linux-arm64@2.53.0': + optional: true + + '@sentry/cli-linux-arm@2.53.0': + optional: true + + '@sentry/cli-linux-i686@2.53.0': + optional: true + + '@sentry/cli-linux-x64@2.53.0': + optional: true + + '@sentry/cli-win32-arm64@2.53.0': + optional: true + + '@sentry/cli-win32-i686@2.53.0': + optional: true + + '@sentry/cli-win32-x64@2.53.0': + optional: true + + '@sentry/cli@2.53.0(encoding@0.1.13)': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.53.0 + '@sentry/cli-linux-arm': 2.53.0 + '@sentry/cli-linux-arm64': 2.53.0 + '@sentry/cli-linux-i686': 2.53.0 + '@sentry/cli-linux-x64': 2.53.0 + '@sentry/cli-win32-arm64': 2.53.0 + '@sentry/cli-win32-i686': 2.53.0 + '@sentry/cli-win32-x64': 2.53.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/core@10.8.0': {} + + '@sentry/react-native@7.0.1(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)': + dependencies: + '@sentry/babel-plugin-component-annotate': 4.3.0 + '@sentry/browser': 10.8.0 + '@sentry/cli': 2.53.0(encoding@0.1.13) + '@sentry/core': 10.8.0 + '@sentry/react': 10.8.0(react@19.0.0) + '@sentry/types': 10.8.0 + react: 19.0.0 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) + optionalDependencies: + expo: 53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/react@10.8.0(react@19.0.0)': + dependencies: + '@sentry/browser': 10.8.0 + '@sentry/core': 10.8.0 + hoist-non-react-statics: 3.3.2 + react: 19.0.0 + + '@sentry/types@10.8.0': + dependencies: + '@sentry/core': 10.8.0 + '@shikijs/core@3.3.0': dependencies: '@shikijs/types': 3.3.0 -- 2.51.2 From 31a40d70aa4d464fed2811dafaa0030616537e0a Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 15 Sep 2025 17:39:21 -0500 Subject: [PATCH 002/525] sentry works! --- js/app/.gitignore | 2 + js/app/app.config.ts | 13 ++ .../components/provider/provider.shared.tsx | 40 +++- js/app/metro.config.js | 11 +- js/app/package.json | 2 +- pnpm-lock.yaml | 207 +++++++++--------- 6 files changed, 168 insertions(+), 107 deletions(-) diff --git a/js/app/.gitignore b/js/app/.gitignore index dfdf4289..b419ac88 100644 --- a/js/app/.gitignore +++ b/js/app/.gitignore @@ -58,3 +58,5 @@ ios android code-signing-dev generated + +.env.local diff --git a/js/app/app.config.ts b/js/app/app.config.ts index 74f9cb3a..de6c2f33 100644 --- a/js/app/app.config.ts +++ b/js/app/app.config.ts @@ -84,6 +84,7 @@ export const versionCode = (verStr: string) => { export default function () { const isProd = process.env["SP_PRODUCTION_RELEASE"] === "true" || !!process.env.CI; + const enableSentry = process.env["SP_ENABLE_SENTRY"] === "true"; const pkg = require("./package.json"); const name = isProd ? "Streamplace" : "Devplace"; let bundle = isProd ? "tv.aquareum" : "tv.aquareum.dev"; @@ -260,6 +261,18 @@ export default function () { [withNotificationsIOS, {}], ] : ["expo-dev-launcher", withoutNotificationsIOS]), + ...(enableSentry + ? [ + [ + "@sentry/react-native/expo", + { + url: "https://sentry.io/", + project: process.env["SP_SENTRY_APP"] || "app", + organization: process.env["SP_SENTRY_ORG"] || "streamplace", + }, + ], + ] + : []), ], experiments: { typedRoutes: true, diff --git a/js/app/components/provider/provider.shared.tsx b/js/app/components/provider/provider.shared.tsx index 00b4e6be..dc8f2d87 100644 --- a/js/app/components/provider/provider.shared.tsx +++ b/js/app/components/provider/provider.shared.tsx @@ -3,6 +3,7 @@ import { LinkingOptions, NavigationContainer, } from "@react-navigation/native"; +import * as Sentry from "@sentry/react-native"; import { ThemeProvider, StreamplaceProvider as ZustandStreamplaceProvider, @@ -16,14 +17,51 @@ import React from "react"; import { Provider as ReduxProvider } from "react-redux"; import { useAppSelector } from "store/hooks"; import { store } from "store/store"; +import { PortalProvider, TamaguiProvider } from "tamagui"; +import config from "tamagui.config"; +import { CurrentToast } from "./CurrentToast"; -export default function Provider({ +export default Sentry.wrap(ProviderInner); + +import * as Application from "expo-application"; +import Constants from "expo-constants"; +import * as Updates from "expo-updates"; +Sentry.setExtras({ + manifest: Updates.manifest, + linkingUri: Constants.linkingUri, +}); +Sentry.setTag("expoChannel", Updates.channel); +Sentry.setTag("appVersion", Application.nativeApplicationVersion); +Sentry.setTag("deviceId", Constants.sessionId); +Sentry.setTag("executionEnvironment", Constants.executionEnvironment); +Sentry.setTag("expoGoVersion", Constants.expoVersion); +Sentry.setTag("expoRuntimeVersion", Constants.expoRuntimeVersion); + +function ProviderInner({ children, linking, }: { children: React.ReactNode; linking: LinkingOptions; }) { + Sentry.init({ + dsn: process.env.EXPO_PUBLIC_SENTRY_DSN || undefined, + // Adds more context data to events (IP address, cookies, user, etc.) + // For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/ + sendDefaultPii: true, + + // Configure Session Replay + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1, + integrations: [ + Sentry.mobileReplayIntegration(), + Sentry.feedbackIntegration(), + ], + + // uncomment the line below to enable Spotlight (https://spotlightjs.com) + spotlight: __DEV__, + }); + return ( diff --git a/js/app/metro.config.js b/js/app/metro.config.js index da4e096d..6ebd391d 100644 --- a/js/app/metro.config.js +++ b/js/app/metro.config.js @@ -1,12 +1,11 @@ -// Learn more https://docs.expo.io/guides/customizing-metro -/** - * @type {import('expo/metro-config').MetroConfig} - */ -const { getDefaultConfig } = require("expo/metro-config"); const { FileStore } = require("metro-cache"); const path = require("path"); +// Enable Tamagui and add nice web support with optimizing compiler + CSS extraction +const { withTamagui } = require("@tamagui/metro-plugin"); -let config = getDefaultConfig(__dirname, { +const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + +let config = getSentryExpoConfig(__dirname, { // [Web-only]: Enables CSS support in Metro. isCSSEnabled: true, }); diff --git a/js/app/package.json b/js/app/package.json index f8eab8a2..99c52a94 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -39,7 +39,7 @@ "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", "@reduxjs/toolkit": "^2.3.0", - "@sentry/react-native": "^7.0.1", + "@sentry/react-native": "^6.14.0", "@streamplace/atproto-oauth-client-react-native": "workspace:*", "@streamplace/components": "workspace:*", "@tanstack/react-query": "^5.59.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abb426b4..1dd139c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,8 +94,8 @@ importers: specifier: ^2.3.0 version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@19.0.0)(redux@5.0.1))(react@19.0.0) '@sentry/react-native': - specifier: ^7.0.1 - version: 7.0.1(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) + specifier: ^6.14.0 + version: 6.14.0(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) '@streamplace/atproto-oauth-client-react-native': specifier: workspace:* version: link:../atproto-oauth-client-react-native @@ -3790,88 +3790,88 @@ packages: '@scure/bip39@1.4.0': resolution: {integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==} - '@sentry-internal/browser-utils@10.8.0': - resolution: {integrity: sha512-FaQX9eefc8sh3h3ZQy16U73KiH0xgDldXnrFiWK6OeWg8X4bJpnYbLqEi96LgHiQhjnnz+UQP1GDzH5oFuu5fA==} - engines: {node: '>=18'} + '@sentry-internal/browser-utils@8.54.0': + resolution: {integrity: sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==} + engines: {node: '>=14.18'} - '@sentry-internal/feedback@10.8.0': - resolution: {integrity: sha512-n7SqgFQItq4QSPG7bCjcZcIwK6AatKnnmSDJ/i6e8jXNIyLwkEuY2NyvTXACxVdO/kafGD5VmrwnTo3Ekc1AMg==} - engines: {node: '>=18'} + '@sentry-internal/feedback@8.54.0': + resolution: {integrity: sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==} + engines: {node: '>=14.18'} - '@sentry-internal/replay-canvas@10.8.0': - resolution: {integrity: sha512-jC4OOwiNgrlIPeXIPMLkaW53BSS1do+toYHoWzzO5AXGpN6jRhanoSj36FpVuH2N3kFnxKVfVxrwh8L+/3vFWg==} - engines: {node: '>=18'} + '@sentry-internal/replay-canvas@8.54.0': + resolution: {integrity: sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==} + engines: {node: '>=14.18'} - '@sentry-internal/replay@10.8.0': - resolution: {integrity: sha512-9+qDEoEjv4VopLuOzK1zM4LcvcUsvB5N0iJ+FRCM3XzzOCbebJOniXTQbt5HflJc3XLnQNKFdKfTfgj8M/0RKQ==} - engines: {node: '>=18'} + '@sentry-internal/replay@8.54.0': + resolution: {integrity: sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==} + engines: {node: '>=14.18'} - '@sentry/babel-plugin-component-annotate@4.3.0': - resolution: {integrity: sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==} + '@sentry/babel-plugin-component-annotate@3.4.0': + resolution: {integrity: sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==} engines: {node: '>= 14'} - '@sentry/browser@10.8.0': - resolution: {integrity: sha512-2J7HST8/ixCaboq17yFn/j/OEokXSXoCBMXRrFx4FKJggKWZ90e2Iau5mP/IPPhrW+W9zCptCgNMY0167wS4qA==} - engines: {node: '>=18'} + '@sentry/browser@8.54.0': + resolution: {integrity: sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==} + engines: {node: '>=14.18'} - '@sentry/cli-darwin@2.53.0': - resolution: {integrity: sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==} + '@sentry/cli-darwin@2.45.0': + resolution: {integrity: sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg==} engines: {node: '>=10'} os: [darwin] - '@sentry/cli-linux-arm64@2.53.0': - resolution: {integrity: sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==} + '@sentry/cli-linux-arm64@2.45.0': + resolution: {integrity: sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg==} engines: {node: '>=10'} cpu: [arm64] - os: [linux, freebsd, android] + os: [linux, freebsd] - '@sentry/cli-linux-arm@2.53.0': - resolution: {integrity: sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==} + '@sentry/cli-linux-arm@2.45.0': + resolution: {integrity: sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ==} engines: {node: '>=10'} cpu: [arm] - os: [linux, freebsd, android] + os: [linux, freebsd] - '@sentry/cli-linux-i686@2.53.0': - resolution: {integrity: sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==} + '@sentry/cli-linux-i686@2.45.0': + resolution: {integrity: sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw==} engines: {node: '>=10'} cpu: [x86, ia32] - os: [linux, freebsd, android] + os: [linux, freebsd] - '@sentry/cli-linux-x64@2.53.0': - resolution: {integrity: sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==} + '@sentry/cli-linux-x64@2.45.0': + resolution: {integrity: sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg==} engines: {node: '>=10'} cpu: [x64] - os: [linux, freebsd, android] + os: [linux, freebsd] - '@sentry/cli-win32-arm64@2.53.0': - resolution: {integrity: sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==} + '@sentry/cli-win32-arm64@2.45.0': + resolution: {integrity: sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@sentry/cli-win32-i686@2.53.0': - resolution: {integrity: sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==} + '@sentry/cli-win32-i686@2.45.0': + resolution: {integrity: sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w==} engines: {node: '>=10'} cpu: [x86, ia32] os: [win32] - '@sentry/cli-win32-x64@2.53.0': - resolution: {integrity: sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==} + '@sentry/cli-win32-x64@2.45.0': + resolution: {integrity: sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@sentry/cli@2.53.0': - resolution: {integrity: sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==} + '@sentry/cli@2.45.0': + resolution: {integrity: sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw==} engines: {node: '>= 10'} hasBin: true - '@sentry/core@10.8.0': - resolution: {integrity: sha512-scYzM/UOItu4PjEq6CpHLdArpXjIS0laHYxE4YjkIbYIH6VMcXGQbD/FSBClsnCr1wXRnlXfXBzj0hrQAFyw+Q==} - engines: {node: '>=18'} + '@sentry/core@8.54.0': + resolution: {integrity: sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==} + engines: {node: '>=14.18'} - '@sentry/react-native@7.0.1': - resolution: {integrity: sha512-xz8ON51qSDvcHVFkdLo0b7rlrQVXpRVXqzm7e1+nHEZ07TX0o+utxx04akxD1Z4hmGPTWPmsHeMlm7diV9NtTQ==} + '@sentry/react-native@6.14.0': + resolution: {integrity: sha512-BBqixN6oV6tCNp1ABXfzvD531zxj1fUAH0HDPvOR/jX0h9f9pYfxCyI64B+DoQbVZKFsg8nte0QIHkZDhRAW9A==} hasBin: true peerDependencies: expo: '>=49.0.0' @@ -3881,15 +3881,19 @@ packages: expo: optional: true - '@sentry/react@10.8.0': - resolution: {integrity: sha512-w/dGLMCLJG2lp8gKVKX1jjeg2inXewKfPb73+PS1CDi9/ihvqZU2DAXxnaNsBA7YYtGwlWVJe1bLAqguwTEpqw==} - engines: {node: '>=18'} + '@sentry/react@8.54.0': + resolution: {integrity: sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w==} + engines: {node: '>=14.18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/types@10.8.0': - resolution: {integrity: sha512-xRe41/KvnNt4o6t5YeB+yBRTWvLUu6FJpft/VBOs4Bfh1/6rz+l78oxSCtpXo3MsfTd5185I0uuggAjEdD4Y6g==} - engines: {node: '>=18'} + '@sentry/types@8.54.0': + resolution: {integrity: sha512-wztdtr7dOXQKi0iRvKc8XJhJ7HaAfOv8lGu0yqFOFwBZucO/SHnu87GOPi8mvrTiy1bentQO5l+zXWAaMvG4uw==} + engines: {node: '>=14.18'} + + '@sentry/utils@8.54.0': + resolution: {integrity: sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw==} + engines: {node: '>=14.18'} '@shikijs/core@3.3.0': resolution: {integrity: sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==} @@ -17263,59 +17267,59 @@ snapshots: '@noble/hashes': 1.5.0 '@scure/base': 1.1.9 - '@sentry-internal/browser-utils@10.8.0': + '@sentry-internal/browser-utils@8.54.0': dependencies: - '@sentry/core': 10.8.0 + '@sentry/core': 8.54.0 - '@sentry-internal/feedback@10.8.0': + '@sentry-internal/feedback@8.54.0': dependencies: - '@sentry/core': 10.8.0 + '@sentry/core': 8.54.0 - '@sentry-internal/replay-canvas@10.8.0': + '@sentry-internal/replay-canvas@8.54.0': dependencies: - '@sentry-internal/replay': 10.8.0 - '@sentry/core': 10.8.0 + '@sentry-internal/replay': 8.54.0 + '@sentry/core': 8.54.0 - '@sentry-internal/replay@10.8.0': + '@sentry-internal/replay@8.54.0': dependencies: - '@sentry-internal/browser-utils': 10.8.0 - '@sentry/core': 10.8.0 + '@sentry-internal/browser-utils': 8.54.0 + '@sentry/core': 8.54.0 - '@sentry/babel-plugin-component-annotate@4.3.0': {} + '@sentry/babel-plugin-component-annotate@3.4.0': {} - '@sentry/browser@10.8.0': + '@sentry/browser@8.54.0': dependencies: - '@sentry-internal/browser-utils': 10.8.0 - '@sentry-internal/feedback': 10.8.0 - '@sentry-internal/replay': 10.8.0 - '@sentry-internal/replay-canvas': 10.8.0 - '@sentry/core': 10.8.0 + '@sentry-internal/browser-utils': 8.54.0 + '@sentry-internal/feedback': 8.54.0 + '@sentry-internal/replay': 8.54.0 + '@sentry-internal/replay-canvas': 8.54.0 + '@sentry/core': 8.54.0 - '@sentry/cli-darwin@2.53.0': + '@sentry/cli-darwin@2.45.0': optional: true - '@sentry/cli-linux-arm64@2.53.0': + '@sentry/cli-linux-arm64@2.45.0': optional: true - '@sentry/cli-linux-arm@2.53.0': + '@sentry/cli-linux-arm@2.45.0': optional: true - '@sentry/cli-linux-i686@2.53.0': + '@sentry/cli-linux-i686@2.45.0': optional: true - '@sentry/cli-linux-x64@2.53.0': + '@sentry/cli-linux-x64@2.45.0': optional: true - '@sentry/cli-win32-arm64@2.53.0': + '@sentry/cli-win32-arm64@2.45.0': optional: true - '@sentry/cli-win32-i686@2.53.0': + '@sentry/cli-win32-i686@2.45.0': optional: true - '@sentry/cli-win32-x64@2.53.0': + '@sentry/cli-win32-x64@2.45.0': optional: true - '@sentry/cli@2.53.0(encoding@0.1.13)': + '@sentry/cli@2.45.0(encoding@0.1.13)': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0(encoding@0.1.13) @@ -17323,28 +17327,29 @@ snapshots: proxy-from-env: 1.1.0 which: 2.0.2 optionalDependencies: - '@sentry/cli-darwin': 2.53.0 - '@sentry/cli-linux-arm': 2.53.0 - '@sentry/cli-linux-arm64': 2.53.0 - '@sentry/cli-linux-i686': 2.53.0 - '@sentry/cli-linux-x64': 2.53.0 - '@sentry/cli-win32-arm64': 2.53.0 - '@sentry/cli-win32-i686': 2.53.0 - '@sentry/cli-win32-x64': 2.53.0 + '@sentry/cli-darwin': 2.45.0 + '@sentry/cli-linux-arm': 2.45.0 + '@sentry/cli-linux-arm64': 2.45.0 + '@sentry/cli-linux-i686': 2.45.0 + '@sentry/cli-linux-x64': 2.45.0 + '@sentry/cli-win32-arm64': 2.45.0 + '@sentry/cli-win32-i686': 2.45.0 + '@sentry/cli-win32-x64': 2.45.0 transitivePeerDependencies: - encoding - supports-color - '@sentry/core@10.8.0': {} + '@sentry/core@8.54.0': {} - '@sentry/react-native@7.0.1(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)': + '@sentry/react-native@6.14.0(encoding@0.1.13)(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)': dependencies: - '@sentry/babel-plugin-component-annotate': 4.3.0 - '@sentry/browser': 10.8.0 - '@sentry/cli': 2.53.0(encoding@0.1.13) - '@sentry/core': 10.8.0 - '@sentry/react': 10.8.0(react@19.0.0) - '@sentry/types': 10.8.0 + '@sentry/babel-plugin-component-annotate': 3.4.0 + '@sentry/browser': 8.54.0 + '@sentry/cli': 2.45.0(encoding@0.1.13) + '@sentry/core': 8.54.0 + '@sentry/react': 8.54.0(react@19.0.0) + '@sentry/types': 8.54.0 + '@sentry/utils': 8.54.0 react: 19.0.0 react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) optionalDependencies: @@ -17353,16 +17358,20 @@ snapshots: - encoding - supports-color - '@sentry/react@10.8.0(react@19.0.0)': + '@sentry/react@8.54.0(react@19.0.0)': dependencies: - '@sentry/browser': 10.8.0 - '@sentry/core': 10.8.0 + '@sentry/browser': 8.54.0 + '@sentry/core': 8.54.0 hoist-non-react-statics: 3.3.2 react: 19.0.0 - '@sentry/types@10.8.0': + '@sentry/types@8.54.0': + dependencies: + '@sentry/core': 8.54.0 + + '@sentry/utils@8.54.0': dependencies: - '@sentry/core': 10.8.0 + '@sentry/core': 8.54.0 '@shikijs/core@3.3.0': dependencies: -- 2.51.2 From 8f141ae03f356e970db1b57921ee4e68c0a29e9e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sat, 20 Sep 2025 15:29:23 -0500 Subject: [PATCH 003/525] sentry dsn injection only on web --- .../components/provider/provider.shared.tsx | 13 +++++++- pkg/api/api.go | 4 +-- pkg/config/config.go | 2 ++ pkg/linking/linking.go | 32 ++++++++++++++----- pkg/linking/linking_test.go | 2 +- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/js/app/components/provider/provider.shared.tsx b/js/app/components/provider/provider.shared.tsx index dc8f2d87..9645d688 100644 --- a/js/app/components/provider/provider.shared.tsx +++ b/js/app/components/provider/provider.shared.tsx @@ -26,6 +26,7 @@ export default Sentry.wrap(ProviderInner); import * as Application from "expo-application"; import Constants from "expo-constants"; import * as Updates from "expo-updates"; +import { Platform } from "react-native"; Sentry.setExtras({ manifest: Updates.manifest, linkingUri: Constants.linkingUri, @@ -44,8 +45,18 @@ function ProviderInner({ children: React.ReactNode; linking: LinkingOptions; }) { + // get proper DSN for environment + // on ios/android it's process.env.EXPO_PUBLIC_SENTRY_DSN + // on web it will be injected at runtime + let dsn = undefined; + if (Platform.OS === "web") { + dsn = (window as any).SENTRY_DSN; + } else { + dsn = process.env.EXPO_PUBLIC_SENTRY_DSN || undefined; + } + Sentry.init({ - dsn: process.env.EXPO_PUBLIC_SENTRY_DSN || undefined, + dsn, // Adds more context data to events (IP address, cookies, user, etc.) // For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/ sendDefaultPii: true, diff --git a/pkg/api/api.go b/pkg/api/api.go index f1d9a991..d25bae5a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -314,7 +314,7 @@ func (a *StreamplaceAPI) NotFoundLinkingHandler(ctx context.Context, linker *lin return } if errors.Is(err, ErrorIndex) || f == "" { - bs, err := linker.GenerateDefaultCard(ctx, req.URL) + bs, err := linker.GenerateDefaultCard(ctx, req.URL, a.CLI.SentryDSN) if err != nil { log.Error(ctx, "error generating default card", "error", err) } @@ -357,7 +357,7 @@ func (a *StreamplaceAPI) NotFoundLinkingHandler(ctx context.Context, linker *lin defaultHandler.ServeHTTP(w, req) return } - bs, err := linker.GenerateStreamerCard(ctx, req.URL, lsv) + bs, err := linker.GenerateStreamerCard(ctx, req.URL, lsv, a.CLI.SentryDSN) if err != nil { log.Error(ctx, "error generating html", "error", err) defaultHandler.ServeHTTP(w, req) diff --git a/pkg/config/config.go b/pkg/config/config.go index b211a116..50696cbf 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -116,6 +116,7 @@ type CLI struct { LivepeerHelp bool PLCURL string SQLLogging bool + SentryDSN string } func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { @@ -180,6 +181,7 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { fs.BoolVar(&cli.LivepeerHelp, "livepeer-help", false, "print help for livepeer flags and exit") fs.StringVar(&cli.PLCURL, "plc-url", "https://plc.directory", "url of the plc directory") fs.BoolVar(&cli.SQLLogging, "sql-logging", false, "enable sql logging") + fs.StringVar(&cli.SentryDSN, "sentry-dsn", "", "sentry dsn for error reporting") lpFlags := flag.NewFlagSet("livepeer", flag.ContinueOnError) _ = starter.NewLivepeerConfig(lpFlags) diff --git a/pkg/linking/linking.go b/pkg/linking/linking.go index d80ebb53..4b9d2fed 100644 --- a/pkg/linking/linking.go +++ b/pkg/linking/linking.go @@ -25,8 +25,9 @@ func NewLinker(ctx context.Context, baseHTML []byte) (*Linker, error) { } type PageConfig struct { - Title string - Metas []MetaTag + Title string + Metas []MetaTag + SentryDSN string } // Define all meta tags in a structured way @@ -36,7 +37,7 @@ type MetaTag struct { Content string } -func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *streamplace.Livestream_LivestreamView) ([]byte, error) { +func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *streamplace.Livestream_LivestreamView, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") } @@ -79,12 +80,13 @@ func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *stre } return l.GenerateHTML(ctx, &PageConfig{ - Title: pageTitle, - Metas: metaTags, + Title: pageTitle, + Metas: metaTags, + SentryDSN: sentryDSN, }) } -func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL) ([]byte, error) { +func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") } @@ -114,8 +116,9 @@ func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL) ([]byte, e } return l.GenerateHTML(ctx, &PageConfig{ - Title: "Stream.place", - Metas: metaTags, + Title: "Stream.place", + Metas: metaTags, + SentryDSN: sentryDSN, }) } @@ -183,6 +186,19 @@ func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, erro }) } + // Add Sentry DSN script if configured + if pc.SentryDSN != "" { + script := &html.Node{ + Type: html.ElementNode, + Data: "script", + } + head.AppendChild(script) + script.AppendChild(&html.Node{ + Type: html.TextNode, + Data: `window.SENTRY_DSN = "` + pc.SentryDSN + `";`, + }) + } + // Render the HTML to a string var buf bytes.Buffer if err := html.Render(&buf, root); err != nil { diff --git a/pkg/linking/linking_test.go b/pkg/linking/linking_test.go index 78987e6e..8b5c9907 100644 --- a/pkg/linking/linking_test.go +++ b/pkg/linking/linking_test.go @@ -62,7 +62,7 @@ func TestGenerateLinkCard(t *testing.T) { Record: &lexutil.LexiconTypeDecoder{Val: ls}, Uri: "at://did:plc:2zmxikig2sj7gqaezl5gntae/place.stream.livestream/3ll5zuop2k22x", } - linkCard, err := linker.GenerateStreamerCard(context.Background(), u, lsv) + linkCard, err := linker.GenerateStreamerCard(context.Background(), u, lsv, "") require.NoError(t, err) linkStr := string(linkCard) require.True(t, strings.Contains(linkStr, "iame.li")) -- 2.51.2 From a9290627a55a10bc47fcb2283989536cca7a5833 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sat, 20 Sep 2025 17:01:17 -0500 Subject: [PATCH 004/525] rm tamagui --- js/app/components/provider/provider.shared.tsx | 3 --- js/app/metro.config.js | 2 -- 2 files changed, 5 deletions(-) diff --git a/js/app/components/provider/provider.shared.tsx b/js/app/components/provider/provider.shared.tsx index 9645d688..c3f6e424 100644 --- a/js/app/components/provider/provider.shared.tsx +++ b/js/app/components/provider/provider.shared.tsx @@ -17,9 +17,6 @@ import React from "react"; import { Provider as ReduxProvider } from "react-redux"; import { useAppSelector } from "store/hooks"; import { store } from "store/store"; -import { PortalProvider, TamaguiProvider } from "tamagui"; -import config from "tamagui.config"; -import { CurrentToast } from "./CurrentToast"; export default Sentry.wrap(ProviderInner); diff --git a/js/app/metro.config.js b/js/app/metro.config.js index 6ebd391d..c2e0556b 100644 --- a/js/app/metro.config.js +++ b/js/app/metro.config.js @@ -1,7 +1,5 @@ const { FileStore } = require("metro-cache"); const path = require("path"); -// Enable Tamagui and add nice web support with optimizing compiler + CSS extraction -const { withTamagui } = require("@tamagui/metro-plugin"); const { getSentryExpoConfig } = require("@sentry/react-native/metro"); -- 2.51.2 From 3f0058461a18a3eae28d399ac4fe924bc372b1e6 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 21 Sep 2025 16:22:31 -0700 Subject: [PATCH 005/525] c2pa: add a single go --> rust c2pa call --- Cargo.lock | 1748 ++++++++++++++++- pkg/cmd/streamplace.go | 3 + .../iroh_streamplace/iroh_streamplace.go | 135 ++ .../iroh_streamplace/iroh_streamplace.h | 11 + rust/iroh-streamplace/Cargo.toml | 5 + rust/iroh-streamplace/src/c2pa.rs | 27 + rust/iroh-streamplace/src/lib.rs | 1 + 7 files changed, 1864 insertions(+), 66 deletions(-) create mode 100644 rust/iroh-streamplace/src/c2pa.rs diff --git a/Cargo.lock b/Cargo.lock index fcec4c1e..ed640205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "abnf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" +dependencies = [ + "abnf-core", + "nom", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + [[package]] name = "acto" version = "0.7.4" @@ -81,6 +100,15 @@ dependencies = [ "backtrace", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -134,6 +162,45 @@ dependencies = [ "nom", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "async-compat" version = "0.2.4" @@ -147,6 +214,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-generic" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3728566eefa873833159754f5732fb0951d3649e6e5b891cc70d56dd41673" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -184,13 +279,19 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atree" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132573478eb9ff973c6f75d0ed425ac12da77d266506483345f46743ecc83a98" + [[package]] name = "attohttpc" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "base64", + "base64 0.22.1", "http", "log", "url", @@ -240,6 +341,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -261,6 +368,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bincode" version = "1.3.3" @@ -275,6 +392,31 @@ name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bitvec-nom2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" +dependencies = [ + "bitvec", + "nom", +] [[package]] name = "blake3" @@ -310,6 +452,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "387e80962b798815a2b5c4bcfdb6bf626fa922ffe9f74e373103b858738e9f31" +[[package]] +name = "btree-range-map" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" +dependencies = [ + "btree-slab", + "cc-traits", + "range-traits", + "serde", + "slab", +] + +[[package]] +name = "btree-slab" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" +dependencies = [ + "cc-traits", + "slab", + "smallvec", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -322,6 +488,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteordered" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf2cd9424f5ff404aba1959c835cbc448ee8b689b870a9981c76c0fd46280e6" +dependencies = [ + "byteorder", +] + [[package]] name = "bytes" version = "1.10.1" @@ -331,6 +506,100 @@ dependencies = [ "serde", ] +[[package]] +name = "c2pa" +version = "0.58.0" +source = "git+https://github.com/hyphacoop/c2pa-rs.git?rev=1b84d40219b27340a30fc309250e774e8a7b7761#1b84d40219b27340a30fc309250e774e8a7b7761" +dependencies = [ + "asn1-rs", + "async-generic", + "async-recursion", + "async-trait", + "atree", + "base64 0.22.1", + "bcder", + "byteorder", + "byteordered", + "bytes", + "chrono", + "ciborium", + "config", + "console_log", + "const-hex", + "const-oid", + "conv", + "coset", + "der", + "ed25519-dalek", + "env_logger", + "extfmt", + "getrandom 0.2.16", + "hex", + "hex-literal", + "http", + "id3", + "img-parts", + "iref", + "jfifdump", + "js-sys", + "lazy_static", + "log", + "memchr", + "mp4", + "nom", + "non-empty-string", + "nonempty-collections", + "num-bigint-dig", + "openssl", + "pem", + "pkcs1", + "pkcs8", + "png_pong", + "quick-xml", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.9.3", + "range-set", + "rasn", + "rasn-cms", + "rasn-ocsp", + "rasn-pkix", + "regex", + "reqwest", + "riff", + "ring", + "rsa", + "serde", + "serde-transcode", + "serde-wasm-bindgen", + "serde_bytes", + "serde_cbor", + "serde_derive", + "serde_json", + "serde_with", + "sha1", + "sha2", + "spki", + "static-iref", + "tempfile", + "thiserror 2.0.16", + "toml 0.8.23", + "treeline", + "ureq", + "url", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-core", + "wstd", + "x509-certificate", + "x509-parser", + "zeroize", + "zip", +] + [[package]] name = "camino" version = "1.1.11" @@ -372,6 +641,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cc-traits" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" +dependencies = [ + "slab", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -403,11 +681,40 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half 2.6.0", +] + [[package]] name = "cipher" version = "0.4.4" @@ -425,7 +732,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -439,18 +746,86 @@ dependencies = [ "termcolor", ] +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.8.23", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "const-hex" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", +] + [[package]] name = "cordyceps" version = "0.3.4" @@ -477,6 +852,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "coset" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -501,6 +886,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -531,6 +925,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -603,12 +1003,64 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.105", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.105", +] + [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "delegate" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "der" version = "0.7.10" @@ -621,6 +1073,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.7.3" @@ -639,6 +1105,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", ] [[package]] @@ -696,6 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -722,6 +1201,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.11" @@ -759,10 +1247,17 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2", + "signature", "subtle", "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embedded-io" version = "0.4.0" @@ -791,27 +1286,56 @@ dependencies = [ name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", -] +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "enumflags2_derive" -version = "0.7.12" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.105", + "libc", + "windows-sys 0.59.0", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "extfmt" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "a48fe53466ab1f4ea6303bf9d7a0ca8060778590f09fd6c3304cc817aeb9935d" [[package]] name = "fallible-iterator" @@ -831,6 +1355,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -843,6 +1377,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -861,6 +1410,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1075,13 +1630,29 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1091,6 +1662,18 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1122,12 +1705,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "hickory-proto" version = "0.25.2" @@ -1146,7 +1747,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tokio", "tracing", @@ -1169,7 +1770,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -1251,6 +1852,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "1.6.0" @@ -1295,7 +1902,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1423,6 +2030,23 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id3" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872718dfcd3a6f4601a3fb413365ee5e1d3ec050316ff79e690c98658a6738ef" +dependencies = [ + "bitflags", + "byteorder", + "flate2", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1465,6 +2089,28 @@ dependencies = [ "xmltree", ] +[[package]] +name = "img-parts" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4e24cfdc6f897b582508e3c382eaf5378076898f80500a80d10d761ae85e90" +dependencies = [ + "bytes", + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -1472,9 +2118,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.5", + "serde", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "inout" version = "0.1.4" @@ -1525,6 +2178,28 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iref" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374372d9ca7331cec26f307b12552554849143e6b2077be3553576aa9aa8258c" +dependencies = [ + "iref-core", +] + +[[package]] +name = "iref-core" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10559a0d518effd4f2cee107f40f83acf8583dcd3e6760b9b60293b0d2c2a70" +dependencies = [ + "pct-str", + "serde", + "smallvec", + "static-regular-grammar", + "thiserror 1.0.69", +] + [[package]] name = "iri-string" version = "0.7.8" @@ -1655,7 +2330,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -1675,7 +2350,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -1752,6 +2427,7 @@ dependencies = [ "anyhow", "async-trait", "bytes", + "c2pa", "iroh", "irpc", "irpc-iroh", @@ -1759,6 +2435,7 @@ dependencies = [ "postcard", "serde", "snafu", + "thiserror 2.0.16", "tokio", "tracing", "tracing-subscriber", @@ -1783,7 +2460,7 @@ dependencies = [ "rustls", "serde", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-util", "tracing", @@ -1817,12 +2494,38 @@ dependencies = [ "tracing", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jfifdump" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cf72bc7b75b6615ffd06bfe6840f3c57e7e4ea615558a2a452f458ebf5551e" + [[package]] name = "js-sys" version = "0.3.77" @@ -1833,11 +2536,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "libc" @@ -1845,6 +2562,18 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "litemap" version = "0.8.0" @@ -1892,7 +2621,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -1951,6 +2680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1983,6 +2713,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "mp4" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3" +dependencies = [ + "byteorder", + "bytes", + "num-rational", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "n0-future" version = "0.1.3" @@ -2142,7 +2886,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -2209,6 +2953,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "non-empty-string" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cf0f4060e345ae505219853da9ca1150564158a648a6aa6a528f0d5794bb33" +dependencies = [ + "delegate", + "serde", +] + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + [[package]] name = "ntimestamp" version = "1.0.0" @@ -2234,12 +2997,72 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2247,6 +3070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2280,6 +3104,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2296,6 +3129,64 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "openssl-src" +version = "300.5.2+3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "overload" version = "0.1.1" @@ -2331,19 +3222,44 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parsenic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c695d2b8bcf1dd62a5173a9c43e6ebbe9261701c002f9b462fc9690c144bb61" +dependencies = [ + "traitful", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pct-str" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1bdcc492c285a50bed60860dfa00b50baf1f60c73c7d6b435b01a2a11fd6ff" +dependencies = [ + "thiserror 1.0.69", + "utf8-decode", +] + [[package]] name = "pem" version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64", + "base64 0.22.1", "serde", ] @@ -2369,7 +3285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.14", + "thiserror 2.0.16", "ucd-trie", ] @@ -2448,6 +3364,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pix" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6574ffbea793ab0c9a9b09ae99831f1513fdd7732b12aca4c7182c46dc3bc9f" + [[package]] name = "pkarr" version = "3.10.0" @@ -2472,13 +3394,24 @@ dependencies = [ "serde", "sha1_smol", "simple-dns", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "url", "wasm-bindgen-futures", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2489,6 +3422,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plain" version = "0.2.3" @@ -2537,6 +3476,19 @@ dependencies = [ "pnet_macros_support", ] +[[package]] +name = "png_pong" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a767b8b17e388cb7f1e74cf3c2ebce14e6b7ae123fa27150889a353fd9eb981f" +dependencies = [ + "miniz_oxide", + "parsenic", + "pix", + "simd-adler32", + "traitful", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -2560,7 +3512,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f1975debe62a70557e42b9ff9466e4890cf9d3d156d296408a711f1c5f642b" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "derive_more 2.0.1", "futures-lite", @@ -2677,6 +3629,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.97" @@ -2686,6 +3662,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +dependencies = [ + "bitflags", + "lazy_static", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.5", + "unarray", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.8" @@ -2700,7 +3701,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -2721,7 +3722,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -2766,6 +3767,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2825,6 +3832,107 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "range-set" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714bc4849c399f77ab82177e9b4f012e02f3a7d6191023e016334edde5b21050" +dependencies = [ + "num-traits", + "smallvec", +] + +[[package]] +name = "range-traits" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" + +[[package]] +name = "rasn" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7863f4e3d3cbbe0d3a427aa48447c42b5a74a579cbfa7afa63735b0c10879c44" +dependencies = [ + "bitvec", + "bitvec-nom2", + "bytes", + "chrono", + "either", + "nom", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rasn-derive", + "serde_json", + "snafu", + "xml-no-std", +] + +[[package]] +name = "rasn-cms" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffab95fefca87ed16fa9501141bebd99643f51d46c7e19f3c0a2c321ae5c27d2" +dependencies = [ + "rasn", + "rasn-pkix", +] + +[[package]] +name = "rasn-derive" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a16f695759320ec969864fe5e7aa8e2a9c5665469565b100b8c8c1a36cd5484" +dependencies = [ + "proc-macro2", + "rasn-derive-impl", + "syn 2.0.105", +] + +[[package]] +name = "rasn-derive-impl" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14454245fd76215b830d083b6b6c6842047e50cb7ee73ce1e769d4b099c89fd" +dependencies = [ + "either", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.105", + "uuid", +] + +[[package]] +name = "rasn-ocsp" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5252a65c8d5c8435420db5417b6a881f907c75a394c5f88cd392118f273ddc55" +dependencies = [ + "rasn", + "rasn-pkix", +] + +[[package]] +name = "rasn-pkix" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c902bfc1add6e7471323cdb879fe6a3c3122b1cb0247937c0940b28e2334252" +dependencies = [ + "rasn", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -2847,6 +3955,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "regex" version = "1.11.1" @@ -2903,7 +4031,7 @@ version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -2944,6 +4072,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +[[package]] +name = "riff" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c601484456988d75017d86700d3743b949c21cdc7399f940c75e34680d185c5" + [[package]] name = "ring" version = "0.17.14" @@ -2958,6 +4092,49 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -2979,6 +4156,28 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.23.31" @@ -2994,6 +4193,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -3036,6 +4244,30 @@ dependencies = [ "cipher", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3072,46 +4304,86 @@ dependencies = [ name = "self_cell" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-transcode" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2" +dependencies = [ + "serde", +] [[package]] -name = "semver" -version = "1.0.26" +name = "serde-wasm-bindgen" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ + "js-sys", "serde", + "wasm-bindgen", ] [[package]] -name = "send_wrapper" -version = "0.6.0" +name = "serde_bytes" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] [[package]] -name = "serde" -version = "1.0.219" +name = "serde_cbor" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" dependencies = [ - "serde_derive", + "half 1.8.3", + "serde", ] [[package]] -name = "serde_bytes" -version = "0.11.17" +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ - "serde", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -3124,12 +4396,22 @@ version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ + "indexmap 2.10.0", "itoa", "memchr", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3142,6 +4424,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "serdect" version = "0.2.0" @@ -3210,9 +4524,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -3331,12 +4652,49 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static-iref" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc4068497ae43896d41174586dcdc2153a1af2c82856fb308bfaaddc28e5549" +dependencies = [ + "iref", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "static-regular-grammar" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" +dependencies = [ + "abnf", + "btree-range-map", + "ciborium", + "hex_fmt", + "indoc", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "sha2", + "syn 2.0.105", + "thiserror 1.0.69", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.2" @@ -3364,7 +4722,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac" dependencies = [ - "base64", + "base64 0.22.1", "bounded-integer", "byteorder", "crc", @@ -3414,7 +4772,7 @@ dependencies = [ "hickory-proto", "rand 0.9.2", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -3488,6 +4846,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.2.16", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3517,11 +4895,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.16", ] [[package]] @@ -3537,9 +4915,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -3562,11 +4940,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", + "itoa", "js-sys", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -3575,6 +4955,25 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -3672,7 +5071,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f29ba084eb43becc9864ba514b4a64f5f65b82f9a6ffbafa5436c1c80605f03" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-sink", @@ -3697,11 +5096,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -3709,11 +5123,20 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.10.0", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -3831,6 +5254,23 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "traitful" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d856e22ead1fb79b9fc3cec63300086f680924f2f7b0e2701f6835a28b9c4425" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + [[package]] name = "try-lock" version = "0.2.5" @@ -3858,6 +5298,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.8.1" @@ -3916,7 +5362,7 @@ dependencies = [ "paste", "serde", "textwrap", - "toml", + "toml 0.5.11", "uniffi_meta", "uniffi_udl", ] @@ -3960,7 +5406,7 @@ dependencies = [ "quote", "serde", "syn 2.0.105", - "toml", + "toml 0.5.11", "uniffi_meta", ] @@ -4018,6 +5464,36 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots 1.0.2", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -4030,6 +5506,18 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4038,12 +5526,12 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ - "getrandom 0.3.3", - "js-sys", + "getrandom 0.2.16", + "serde", "wasm-bindgen", ] @@ -4053,6 +5541,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -4083,6 +5577,15 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -4529,6 +6032,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4548,7 +6060,7 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.14", + "thiserror 2.0.16", "windows", "windows-core", ] @@ -4572,12 +6084,90 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 2.0.14", + "thiserror 2.0.16", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] +[[package]] +name = "wstd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0736607b57fcb58dd3148cf34d6a6ca63ba041fde8a12ab3f2c48ddf6d11877" +dependencies = [ + "async-task", + "http", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb142608f932022fa7d155d8ed99649d02c56a50532e71913a5a03c7c4e288d3" +dependencies = [ + "quote", + "syn 2.0.105", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-certificate" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57b9f8bcae7c1f36479821ae826d75050c60ce55146fd86d3553ed2573e2762" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "xml-no-std" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd223bc94c615fc02bf2f4bbc22a4a9bfe489c2add3ec10b1038df3aca44cac7" + [[package]] name = "xml-rs" version = "0.8.27" @@ -4678,6 +6268,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] [[package]] name = "zerotrie" @@ -4711,3 +6315,15 @@ dependencies = [ "quote", "syn 2.0.105", ] + +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.10.0", + "memchr", +] diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index fb390600..d9e0db95 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -44,6 +44,7 @@ import ( "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/model" + "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" _ "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" ) @@ -52,6 +53,8 @@ type jobFunc func(ctx context.Context, cli *config.CLI) error // parse the CLI and fire up an streamplace node! func start(build *config.BuildFlags, platformJobs []jobFunc) error { + rustErr := iroh_streamplace.PrintCert("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") + panic(rustErr.Error()) selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" err := media.RunSelfTest(context.Background()) if err != nil { diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index 8cac2cd9..751c8e6a 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -349,6 +349,15 @@ func uniffiCheckChecksums() { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: UniFFI contract version mismatch") } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_func_print_cert() + }) + if checksum != 62027 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_print_cert: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_datahandler_handle_data() @@ -1592,6 +1601,124 @@ func (_ FfiDestroyerSender) Destroy(value *Sender) { value.Destroy() } +type CertError struct { + err error +} + +// Convience method to turn *CertError into error +// Avoiding treating nil pointer as non nil error interface +func (err *CertError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err CertError) Error() string { + return fmt.Sprintf("CertError: %s", err.err.Error()) +} + +func (err CertError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrCertErrorNoCertificateChainFound = fmt.Errorf("CertErrorNoCertificateChainFound") +var ErrCertErrorC2paError = fmt.Errorf("CertErrorC2paError") + +// Variant structs +type CertErrorNoCertificateChainFound struct { + message string +} + +func NewCertErrorNoCertificateChainFound() *CertError { + return &CertError{err: &CertErrorNoCertificateChainFound{}} +} + +func (e CertErrorNoCertificateChainFound) destroy() { +} + +func (err CertErrorNoCertificateChainFound) Error() string { + return fmt.Sprintf("NoCertificateChainFound: %s", err.message) +} + +func (self CertErrorNoCertificateChainFound) Is(target error) bool { + return target == ErrCertErrorNoCertificateChainFound +} + +type CertErrorC2paError struct { + message string +} + +func NewCertErrorC2paError() *CertError { + return &CertError{err: &CertErrorC2paError{}} +} + +func (e CertErrorC2paError) destroy() { +} + +func (err CertErrorC2paError) Error() string { + return fmt.Sprintf("C2paError: %s", err.message) +} + +func (self CertErrorC2paError) Is(target error) bool { + return target == ErrCertErrorC2paError +} + +type FfiConverterCertError struct{} + +var FfiConverterCertErrorINSTANCE = FfiConverterCertError{} + +func (c FfiConverterCertError) Lift(eb RustBufferI) *CertError { + return LiftFromRustBuffer[*CertError](c, eb) +} + +func (c FfiConverterCertError) Lower(value *CertError) C.RustBuffer { + return LowerIntoRustBuffer[*CertError](c, value) +} + +func (c FfiConverterCertError) Read(reader io.Reader) *CertError { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &CertError{&CertErrorNoCertificateChainFound{message}} + case 2: + return &CertError{&CertErrorC2paError{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterCertError.Read()", errorID)) + } + +} + +func (c FfiConverterCertError) Write(writer io.Writer, value *CertError) { + switch variantValue := value.err.(type) { + case *CertErrorNoCertificateChainFound: + writeInt32(writer, 1) + case *CertErrorC2paError: + writeInt32(writer, 2) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterCertError.Write", value)) + } +} + +type FfiDestroyerCertError struct{} + +func (_ FfiDestroyerCertError) Destroy(value *CertError) { + switch variantValue := value.err.(type) { + case CertErrorNoCertificateChainFound: + variantValue.destroy() + case CertErrorC2paError: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerCertError.Destroy", value)) + } +} + // An Error. type Error struct { err error @@ -1983,3 +2110,11 @@ func iroh_streamplace_uniffiFreeGorutine(data C.uint64_t) { guard := handle.Value().(chan struct{}) guard <- struct{}{} } + +func PrintCert(path string) *CertError { + _, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) bool { + C.uniffi_iroh_streamplace_fn_func_print_cert(FfiConverterStringINSTANCE.Lower(path), _uniffiStatus) + return false + }) + return _uniffiErr +} diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 56dec741..3f72811d 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -571,6 +571,11 @@ uint64_t uniffi_iroh_streamplace_fn_method_sender_node_addr(void* ptr uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key, RustBuffer data ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT +void uniffi_iroh_streamplace_fn_func_print_cert(RustBuffer path, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC #define UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC RustBuffer ffi_iroh_streamplace_rustbuffer_alloc(uint64_t size, RustCallStatus *out_status @@ -849,6 +854,12 @@ void ffi_iroh_streamplace_rust_future_free_void(uint64_t handle #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID #define UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID void ffi_iroh_streamplace_rust_future_complete_void(uint64_t handle, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_PRINT_CERT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_PRINT_CERT +uint16_t uniffi_iroh_streamplace_checksum_func_print_cert(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLER_HANDLE_DATA diff --git a/rust/iroh-streamplace/Cargo.toml b/rust/iroh-streamplace/Cargo.toml index f9658a36..d59952e4 100644 --- a/rust/iroh-streamplace/Cargo.toml +++ b/rust/iroh-streamplace/Cargo.toml @@ -23,6 +23,11 @@ serde = { version = "1.0.219", features = ["derive"] } postcard = { version = "1.1.3", features = ["use-std"] } irpc = "0.7.0" irpc-iroh = "0.7.0" +c2pa = { git = "https://github.com/hyphacoop/c2pa-rs.git", rev = "1b84d40219b27340a30fc309250e774e8a7b7761", features = [ + "openssl", + "file_io", +] } +thiserror = "2.0.16" [dev-dependencies] tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs new file mode 100644 index 00000000..caba2c4b --- /dev/null +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -0,0 +1,27 @@ +use c2pa::{ + Builder, ClaimGeneratorInfo, Error, Ingredient, ManifestDefinition, Reader, Signer, + identity::validator::CawgValidator, settings::Settings, +}; + +#[derive(Debug, thiserror::Error, uniffi::Error)] +#[uniffi(flat_error)] +pub enum CertError { + #[error("No certificate chain found")] + NoCertificateChainFound, + #[error("C2PA error: {0}")] + C2paError(String), +} + +#[uniffi::export] +pub fn print_cert(path: &str) -> Result<(), CertError> { + let reader = Reader::from_file(path).map_err(|e| CertError::C2paError(e.to_string()))?; + // todo: add cawg certs here?? + if let Some(manifest) = reader.active_manifest() { + if let Some(si) = manifest.signature_info() { + println!("{}", si.cert_chain()); + // todo: add ocsp validation info + return Ok(()); + } + } + Err(CertError::NoCertificateChainFound) +} diff --git a/rust/iroh-streamplace/src/lib.rs b/rust/iroh-streamplace/src/lib.rs index 50790b8c..11534766 100644 --- a/rust/iroh-streamplace/src/lib.rs +++ b/rust/iroh-streamplace/src/lib.rs @@ -1,5 +1,6 @@ uniffi::setup_scaffolding!(); +pub mod c2pa; pub mod endpoint; pub mod error; pub mod key; -- 2.51.2 From 39c5c944f5804b8b3a31c80816e7d3e221b3bb2b Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 21 Sep 2025 16:39:56 -0700 Subject: [PATCH 006/525] rust: integrate cargo check, cargo fix --- Makefile | 2 ++ pkg/cmd/streamplace.go | 1 - rust/iroh-streamplace/src/c2pa.rs | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index ba88047a..a9541511 100644 --- a/Makefile +++ b/Makefile @@ -312,11 +312,13 @@ check: install $(MAKE) golangci-lint pnpm run check if [ "`gofmt -l . | wc -l`" -gt 0 ]; then echo 'gofmt failed, run make fix'; exit 1; fi + RUSTFLAGS="-D warnings" cargo check .PHONY: fix fix: pnpm run fix gofmt -w . + cargo fix --allow-dirty .PHONY: golangci-lint golangci-lint: diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index d9e0db95..3dbdfa13 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -45,7 +45,6 @@ import ( "stream.place/streamplace/pkg/model" "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" - _ "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" ) // Additional jobs that can be injected by platforms diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs index caba2c4b..de94aca8 100644 --- a/rust/iroh-streamplace/src/c2pa.rs +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -1,7 +1,4 @@ -use c2pa::{ - Builder, ClaimGeneratorInfo, Error, Ingredient, ManifestDefinition, Reader, Signer, - identity::validator::CawgValidator, settings::Settings, -}; +use c2pa::Reader; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] -- 2.51.2 From 8f41f59a14c3f20b405ecafe710065a191e6afec Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sun, 21 Sep 2025 19:27:20 -0500 Subject: [PATCH 007/525] fix sidebar going >250px when active --- js/app/features/base/sidebarSlice.tsx | 2 ++ js/app/src/router.tsx | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/js/app/features/base/sidebarSlice.tsx b/js/app/features/base/sidebarSlice.tsx index 90de866a..7a93adf6 100644 --- a/js/app/features/base/sidebarSlice.tsx +++ b/js/app/features/base/sidebarSlice.tsx @@ -30,6 +30,8 @@ function verifySidebarState(state: any): SidebarState { if (!verifiedState.isHidden) { if (verifiedState.targetWidth < 64) { verifiedState.targetWidth = 64; + } else if (verifiedState.targetWidth > 250) { + verifiedState.targetWidth = 250; } } else { verifiedState.targetWidth = 0; diff --git a/js/app/src/router.tsx b/js/app/src/router.tsx index e54dfcab..c6161d2e 100644 --- a/js/app/src/router.tsx +++ b/js/app/src/router.tsx @@ -401,9 +401,8 @@ export function StreamplaceDrawer() { swipeEnabled: !sidebar.isActive, drawerStyle: { // afaict the drawer is a RN Animated component internally - width: sidebar.isActive - ? (sidebar.animatedWidth as any) - : undefined, + zIndex: 128000, + width: sidebar.isActive ? sidebar.animatedWidth.get() : undefined, }, // rest headerLeft: () => ( -- 2.51.2 From a6cfe4067e15fb8cb8dd4741612231253d59dda7 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 21 Sep 2025 17:30:26 -0700 Subject: [PATCH 008/525] add json schemas --- pkg/c2pa/Builder.schema.json | 1345 ++++++++++++++++++++ pkg/c2pa/ManifestDefinition.schema.json | 1271 +++++++++++++++++++ pkg/c2pa/Reader.schema.json | 1524 +++++++++++++++++++++++ 3 files changed, 4140 insertions(+) create mode 100644 pkg/c2pa/Builder.schema.json create mode 100644 pkg/c2pa/ManifestDefinition.schema.json create mode 100644 pkg/c2pa/Reader.schema.json diff --git a/pkg/c2pa/Builder.schema.json b/pkg/c2pa/Builder.schema.json new file mode 100644 index 00000000..02f6a282 --- /dev/null +++ b/pkg/c2pa/Builder.schema.json @@ -0,0 +1,1345 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Builder", + "description": "Use a Builder to add a signed manifest to an asset.\n\n# Example: Building and signing a manifest\n\n``` use c2pa::Result; use std::path::PathBuf;\n\nuse c2pa::{create_signer, Builder, SigningAlg}; use serde::Serialize; use serde_json::json; use tempfile::tempdir;\n\n#[derive(Serialize)] struct Test { my_tag: usize, }\n\n# fn main() -> Result<()> { #[cfg(feature = \"file_io\")] { let manifest_json = json!({ \"claim_generator_info\": [ { \"name\": \"c2pa_test\", \"version\": \"1.0.0\" } ], \"title\": \"Test_Manifest\" }).to_string();\n\nlet mut builder = Builder::from_json(&manifest_json)?; builder.add_assertion(\"org.contentauth.test\", &Test { my_tag: 42 })?;\n\nlet source = PathBuf::from(\"tests/fixtures/C.jpg\"); let dir = tempdir()?; let dest = dir.path().join(\"test_file.jpg\");\n\n// Create a ps256 signer using certs and key files. TO DO: Update example. let signcert_path = \"tests/fixtures/certs/ps256.pub\"; let pkey_path = \"tests/fixtures/certs/ps256.pem\"; let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;\n\n// embed a manifest using the signer builder.sign_file( signer.as_ref(), &source, &dest)?; } # Ok(()) # } ```", + "type": "object", + "required": [ + "no_embed" + ], + "properties": { + "assertions": { + "description": "A list of assertions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/AssertionDefinition" + } + }, + "base_path": { + "description": "Base path to search for resources.", + "type": [ + "string", + "null" + ] + }, + "builder_flow": { + "description": "The type of builder being used.", + "anyOf": [ + { + "$ref": "#/definitions/BuilderFlow" + }, + { + "type": "null" + } + ] + }, + "claim_generator_info": { + "description": "Claim Generator Info is always required with at least one entry", + "default": [ + { + "name": "c2pa-rs", + "version": "0.58.0" + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/ClaimGeneratorInfo" + } + }, + "claim_version": { + "description": "The version of the claim. Defaults to 1.", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "format": { + "description": "The format of the source file as a MIME type.", + "default": "application/octet-stream", + "type": "string" + }, + "ingredients": { + "description": "A List of ingredients", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Ingredient" + } + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "default": "xmp:iid:4caa88cb-6034-4d42-8969-409379d34945", + "type": "string" + }, + "label": { + "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssertionMetadata" + } + }, + "no_embed": { + "description": "If true, the manifest store will not be embedded in the asset on sign", + "type": "boolean" + }, + "redactions": { + "description": "A list of redactions - URIs to redacted assertions.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "remote_url": { + "description": "Optional remote URL for the manifest", + "type": [ + "string", + "null" + ] + }, + "thumbnail": { + "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "vendor": { + "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", + "type": [ + "string", + "null" + ] + } + }, + "definitions": { + "Actor": { + "description": "Identifies a person responsible for an action.", + "type": "object", + "properties": { + "credentials": { + "description": "List of references to W3C Verifiable Credentials.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HashedUri" + } + }, + "identifier": { + "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", + "type": [ + "string", + "null" + ] + } + } + }, + "AssertionData": { + "description": "This allows the assertion to be expressed as CBOR or JSON. The default is CBOR unless you specify that an assertion should be JSON.", + "anyOf": [ + true + ] + }, + "AssertionDefinition": { + "description": "Defines an assertion that consists of a label that can be either a C2PA-defined assertion label or a custom label in reverse domain format.", + "type": "object", + "required": [ + "data", + "label" + ], + "properties": { + "data": { + "$ref": "#/definitions/AssertionData" + }, + "label": { + "type": "string" + } + } + }, + "AssertionMetadata": { + "description": "The AssertionMetadata structure can be used as part of other assertions or on its own to reference others", + "type": "object", + "properties": { + "dataSource": { + "anyOf": [ + { + "$ref": "#/definitions/DataSource" + }, + { + "type": "null" + } + ] + }, + "dateTime": { + "anyOf": [ + { + "$ref": "#/definitions/DateT" + }, + { + "type": "null" + } + ] + }, + "reference": { + "anyOf": [ + { + "$ref": "#/definitions/HashedUri" + }, + { + "type": "null" + } + ] + }, + "regionOfInterest": { + "anyOf": [ + { + "$ref": "#/definitions/RegionOfInterest" + }, + { + "type": "null" + } + ] + }, + "reviewRatings": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ReviewRating" + } + } + } + }, + "AssetType": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] + } + } + }, + "BuilderFlow": { + "description": "Represents the type of builder flow being used.\n\nThis determines how the builder will be used, such as creating a new asset, opening an existing asset, or updating an existing asset.", + "oneOf": [ + { + "type": "string", + "enum": [ + "Open", + "Update" + ] + }, + { + "type": "object", + "required": [ + "Create" + ], + "properties": { + "Create": { + "$ref": "#/definitions/DigitalSourceType" + } + }, + "additionalProperties": false + } + ] + }, + "ClaimGeneratorInfo": { + "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", + "type": "object", + "required": [ + "name" + ], + "properties": { + "icon": { + "description": "hashed URI to the icon (either embedded or remote)", + "anyOf": [ + { + "$ref": "#/definitions/UriOrResource" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A human readable string naming the claim_generator", + "type": "string" + }, + "operating_system": { + "description": "A human readable string of the OS the claim generator is running on", + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "A human readable string of the product's version", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": true + }, + "Coordinate": { + "description": "An x, y coordinate used for specifying vertices in polygons.", + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "description": "The coordinate along the x-axis.", + "type": "number", + "format": "double" + }, + "y": { + "description": "The coordinate along the y-axis.", + "type": "number", + "format": "double" + } + } + }, + "DataSource": { + "description": "A description of the source for assertion data", + "type": "object", + "required": [ + "type" + ], + "properties": { + "actors": { + "description": "A list of [`Actor`]s associated with this source.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Actor" + } + }, + "details": { + "description": "A human-readable string giving details about the source of the assertion data.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "A value from among the enumerated list indicating the source of the assertion.", + "type": "string" + } + } + }, + "DateT": { + "type": "string" + }, + "DigitalSourceType": { + "oneOf": [ + { + "description": "Media whose digital content is effectively empty, such as a blank canvas or zero-length video.", + "type": "string", + "enum": [ + "Empty" + ] + }, + { + "description": "Data that is the result of algorithmically using a model derived from sampled content and data. Differs from trainedAlgorithmicMedia in that the result isn’t a media type (e.g., image or video) but is a data format (e.g., CSV, pickle).", + "type": "string", + "enum": [ + "TrainedAlgorithmicData" + ] + } + ] + }, + "Frame": { + "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", + "type": "object", + "properties": { + "end": { + "description": "The end of the frame inclusive or the end of the asset if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "start": { + "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "HashedUri": { + "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", + "type": "object", + "required": [ + "hash", + "url" + ], + "properties": { + "alg": { + "description": "A string identifying the cryptographic hash algorithm used to compute the hash", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "Byte string containing the hash value", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "url": { + "description": "JUMBF URI reference", + "type": "string" + } + } + }, + "Ingredient": { + "description": "An `Ingredient` is any external asset that has been used in the creation of an asset.", + "type": "object", + "properties": { + "active_manifest": { + "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", + "type": [ + "string", + "null" + ] + }, + "data": { + "description": "A reference to the actual data of the ingredient.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "data_types": { + "description": "Additional information about the data's type to the ingredient V2 structure.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "description": { + "description": "Additional description of the ingredient.", + "type": [ + "string", + "null" + ] + }, + "document_id": { + "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "format": { + "description": "The format of the source file as a MIME type.", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "An optional hash of the asset to prevent duplicates.", + "type": [ + "string", + "null" + ] + }, + "informational_URI": { + "description": "URI to an informational page about the ingredient or its data.", + "type": [ + "string", + "null" + ] + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "label": { + "description": "The ingredient's label as assigned in the manifest.", + "type": [ + "string", + "null" + ] + }, + "manifest_data": { + "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Any additional [`Metadata`] as defined in the C2PA spec.\n\n[`Metadata`]: crate::Metadata", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "provenance": { + "description": "URI from `dcterms:provenance` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "relationship": { + "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", + "default": "componentOf", + "allOf": [ + { + "$ref": "#/definitions/Relationship" + } + ] + }, + "resources": { + "readOnly": true, + "allOf": [ + { + "$ref": "#/definitions/ResourceStore" + } + ] + }, + "thumbnail": { + "description": "A thumbnail image capturing the visual state at the time of import.\n\nA tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "validation_results": { + "description": "Validation results (Ingredient.V3)", + "anyOf": [ + { + "$ref": "#/definitions/ValidationResults" + }, + { + "type": "null" + } + ] + }, + "validation_status": { + "description": "Validation status (Ingredient v1 & v2)", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "IngredientDeltaValidationResult": { + "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", + "type": "object", + "required": [ + "ingredientAssertionURI", + "validationDeltas" + ], + "properties": { + "ingredientAssertionURI": { + "description": "JUMBF URI reference to the ingredient assertion", + "type": "string" + }, + "validationDeltas": { + "description": "Validation results for the ingredient's active manifest", + "allOf": [ + { + "$ref": "#/definitions/StatusCodes" + } + ] + } + } + }, + "Item": { + "description": "Description of the boundaries of an identified range.", + "type": "object", + "required": [ + "identifier", + "value" + ], + "properties": { + "identifier": { + "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", + "type": "string" + }, + "value": { + "description": "The value of the identifier, e.g. a value of \"2\" for an identifier of \"track_id\" would imply track 2 of the asset.", + "type": "string" + } + } + }, + "Range": { + "description": "A spatial, temporal, frame, or textual range describing the region of interest.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "frame": { + "description": "A frame range.", + "anyOf": [ + { + "$ref": "#/definitions/Frame" + }, + { + "type": "null" + } + ] + }, + "item": { + "description": "A item identifier.", + "anyOf": [ + { + "$ref": "#/definitions/Item" + }, + { + "type": "null" + } + ] + }, + "shape": { + "description": "A spatial range.", + "anyOf": [ + { + "$ref": "#/definitions/Shape" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "A textual range.", + "anyOf": [ + { + "$ref": "#/definitions/Text" + }, + { + "type": "null" + } + ] + }, + "time": { + "description": "A temporal range.", + "anyOf": [ + { + "$ref": "#/definitions/Time" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "The type of range of interest.", + "allOf": [ + { + "$ref": "#/definitions/RangeType" + } + ] + } + } + }, + "RangeType": { + "description": "The type of range for the region of interest.", + "oneOf": [ + { + "description": "A spatial range, see [`Shape`] for more details.", + "type": "string", + "enum": [ + "spatial" + ] + }, + { + "description": "A temporal range, see [`Time`] for more details.", + "type": "string", + "enum": [ + "temporal" + ] + }, + { + "description": "A spatial range, see [`Frame`] for more details.", + "type": "string", + "enum": [ + "frame" + ] + }, + { + "description": "A textual range, see [`Text`] for more details.", + "type": "string", + "enum": [ + "textual" + ] + }, + { + "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", + "type": "string", + "enum": [ + "identified" + ] + } + ] + }, + "RegionOfInterest": { + "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", + "type": "object", + "required": [ + "region" + ], + "properties": { + "description": { + "description": "A free-text string.", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "Additional information about the asset.", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", + "type": [ + "string", + "null" + ] + }, + "region": { + "description": "A range describing the region of interest for the specific asset.", + "type": "array", + "items": { + "$ref": "#/definitions/Range" + } + }, + "role": { + "description": "A value from our controlled vocabulary or an entity-specific value (e.g., com.litware.coolArea) that represents the role of a region among other regions.", + "anyOf": [ + { + "$ref": "#/definitions/Role" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", + "type": [ + "string", + "null" + ] + } + } + }, + "Relationship": { + "description": "The relationship of the ingredient to the current asset.", + "oneOf": [ + { + "description": "The current asset is derived from this ingredient.", + "type": "string", + "enum": [ + "parentOf" + ] + }, + { + "description": "The current asset is a part of this ingredient.", + "type": "string", + "enum": [ + "componentOf" + ] + }, + { + "description": "The ingredient was used as an input to a computational process to create or modify the asset.", + "type": "string", + "enum": [ + "inputTo" + ] + } + ] + }, + "ResourceRef": { + "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", + "type": "object", + "required": [ + "format", + "identifier" + ], + "properties": { + "alg": { + "description": "The algorithm used to hash the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "data_types": { + "description": "More detailed data types as defined in the C2PA spec.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "format": { + "description": "The mime type of the referenced resource.", + "type": "string" + }, + "hash": { + "description": "The hash of the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", + "type": "string" + } + } + }, + "ResourceStore": { + "description": "Resource store to contain binary objects referenced from JSON serializable structures", + "type": "object", + "required": [ + "resources" + ], + "properties": { + "base_path": { + "type": [ + "string", + "null" + ] + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "resources": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + } + } + }, + "ReviewRating": { + "description": "A rating on an Assertion.\n\nSee .", + "type": "object", + "required": [ + "explanation", + "value" + ], + "properties": { + "code": { + "type": [ + "string", + "null" + ] + }, + "explanation": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "Role": { + "description": "A role describing the region.", + "oneOf": [ + { + "description": "Arbitrary area worth identifying.", + "type": "string", + "enum": [ + "c2pa.areaOfInterest" + ] + }, + { + "description": "This area is all that is left after a crop action.", + "type": "string", + "enum": [ + "c2pa.cropped" + ] + }, + { + "description": "This area has had edits applied to it.", + "type": "string", + "enum": [ + "c2pa.edited" + ] + }, + { + "description": "The area where an ingredient was placed/added.", + "type": "string", + "enum": [ + "c2pa.placed" + ] + }, + { + "description": "Something in this area was redacted.", + "type": "string", + "enum": [ + "c2pa.redacted" + ] + }, + { + "description": "Area specific to a subject (human or not).", + "type": "string", + "enum": [ + "c2pa.subjectArea" + ] + }, + { + "description": "A range of information was removed/deleted.", + "type": "string", + "enum": [ + "c2pa.deleted" + ] + }, + { + "description": "Styling was applied to this area.", + "type": "string", + "enum": [ + "c2pa.styled" + ] + }, + { + "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", + "type": "string", + "enum": [ + "c2pa.watermarked" + ] + } + ] + }, + "Shape": { + "description": "A spatial range representing rectangle, circle, or a polygon.", + "type": "object", + "required": [ + "origin", + "type", + "unit" + ], + "properties": { + "height": { + "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "inside": { + "description": "If the range is inside the shape.\n\nThe default value is true.", + "type": [ + "boolean", + "null" + ] + }, + "origin": { + "description": "THe origin of the coordinate in the shape.", + "allOf": [ + { + "$ref": "#/definitions/Coordinate" + } + ] + }, + "type": { + "description": "The type of shape.", + "allOf": [ + { + "$ref": "#/definitions/ShapeType" + } + ] + }, + "unit": { + "description": "The type of unit for the shape range.", + "allOf": [ + { + "$ref": "#/definitions/UnitType" + } + ] + }, + "vertices": { + "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Coordinate" + } + }, + "width": { + "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + } + } + }, + "ShapeType": { + "description": "The type of shape for the range.", + "oneOf": [ + { + "description": "A rectangle.", + "type": "string", + "enum": [ + "rectangle" + ] + }, + { + "description": "A circle.", + "type": "string", + "enum": [ + "circle" + ] + }, + { + "description": "A polygon.", + "type": "string", + "enum": [ + "polygon" + ] + } + ] + }, + "StatusCodes": { + "description": "Contains a set of success, informational, and failure validation status codes.", + "type": "object", + "required": [ + "failure", + "informational", + "success" + ], + "properties": { + "failure": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "informational": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "success": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "Text": { + "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", + "type": "object", + "required": [ + "selectors" + ], + "properties": { + "selectors": { + "description": "The ranges of text to select.", + "type": "array", + "items": { + "$ref": "#/definitions/TextSelectorRange" + } + } + } + }, + "TextSelector": { + "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", + "type": "object", + "required": [ + "fragment" + ], + "properties": { + "end": { + "description": "The end character offset or the end of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "fragment": { + "description": "Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O.", + "type": "string" + }, + "start": { + "description": "The start character offset or the start of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "TextSelectorRange": { + "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", + "type": "object", + "required": [ + "selector" + ], + "properties": { + "end": { + "description": "The end of the text range.", + "anyOf": [ + { + "$ref": "#/definitions/TextSelector" + }, + { + "type": "null" + } + ] + }, + "selector": { + "description": "The start (or entire) text range.", + "allOf": [ + { + "$ref": "#/definitions/TextSelector" + } + ] + } + } + }, + "Time": { + "description": "A temporal range representing a starting time to an ending time.", + "type": "object", + "properties": { + "end": { + "description": "The end time or the end of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "start": { + "description": "The start time or the start of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "The type of time.", + "default": "npt", + "allOf": [ + { + "$ref": "#/definitions/TimeType" + } + ] + } + } + }, + "TimeType": { + "description": "The type of time.", + "oneOf": [ + { + "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", + "type": "string", + "enum": [ + "npt" + ] + } + ] + }, + "UnitType": { + "description": "The type of unit for the range.", + "oneOf": [ + { + "description": "Use pixels.", + "type": "string", + "enum": [ + "pixel" + ] + }, + { + "description": "Use percentage.", + "type": "string", + "enum": [ + "percent" + ] + } + ] + }, + "UriOrResource": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "$ref": "#/definitions/HashedUri" + } + ] + }, + "ValidationResults": { + "description": "A map of validation results for a manifest store.\n\nThe map contains the validation results for the active manifest and any ingredient deltas. It is normal for there to be many", + "type": "object", + "properties": { + "activeManifest": { + "anyOf": [ + { + "$ref": "#/definitions/StatusCodes" + }, + { + "type": "null" + } + ] + }, + "ingredientDeltas": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/IngredientDeltaValidationResult" + } + } + } + }, + "ValidationStatus": { + "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + }, + "explanation": { + "type": [ + "string", + "null" + ] + }, + "success": { + "writeOnly": true, + "type": [ + "boolean", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + } + } + } + } +} \ No newline at end of file diff --git a/pkg/c2pa/ManifestDefinition.schema.json b/pkg/c2pa/ManifestDefinition.schema.json new file mode 100644 index 00000000..89dcdc9a --- /dev/null +++ b/pkg/c2pa/ManifestDefinition.schema.json @@ -0,0 +1,1271 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ManifestDefinition", + "description": "Use a ManifestDefinition to define a manifest and to build a `ManifestStore`. A manifest is a collection of ingredients and assertions used to define a claim that can be signed and embedded into a file.", + "type": "object", + "properties": { + "assertions": { + "description": "A list of assertions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/AssertionDefinition" + } + }, + "claim_generator_info": { + "description": "Claim Generator Info is always required with at least one entry", + "default": [ + { + "name": "c2pa-rs", + "version": "0.58.0" + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/ClaimGeneratorInfo" + } + }, + "claim_version": { + "description": "The version of the claim. Defaults to 1.", + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "format": { + "description": "The format of the source file as a MIME type.", + "default": "application/octet-stream", + "type": "string" + }, + "ingredients": { + "description": "A List of ingredients", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Ingredient" + } + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "default": "xmp:iid:36dc43ae-f0d2-4b76-a04f-29ee2f27d67a", + "type": "string" + }, + "label": { + "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssertionMetadata" + } + }, + "redactions": { + "description": "A list of redactions - URIs to redacted assertions.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "thumbnail": { + "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "vendor": { + "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", + "type": [ + "string", + "null" + ] + } + }, + "definitions": { + "Actor": { + "description": "Identifies a person responsible for an action.", + "type": "object", + "properties": { + "credentials": { + "description": "List of references to W3C Verifiable Credentials.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HashedUri" + } + }, + "identifier": { + "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", + "type": [ + "string", + "null" + ] + } + } + }, + "AssertionData": { + "description": "This allows the assertion to be expressed as CBOR or JSON. The default is CBOR unless you specify that an assertion should be JSON.", + "anyOf": [ + true + ] + }, + "AssertionDefinition": { + "description": "Defines an assertion that consists of a label that can be either a C2PA-defined assertion label or a custom label in reverse domain format.", + "type": "object", + "required": [ + "data", + "label" + ], + "properties": { + "data": { + "$ref": "#/definitions/AssertionData" + }, + "label": { + "type": "string" + } + } + }, + "AssertionMetadata": { + "description": "The AssertionMetadata structure can be used as part of other assertions or on its own to reference others", + "type": "object", + "properties": { + "dataSource": { + "anyOf": [ + { + "$ref": "#/definitions/DataSource" + }, + { + "type": "null" + } + ] + }, + "dateTime": { + "anyOf": [ + { + "$ref": "#/definitions/DateT" + }, + { + "type": "null" + } + ] + }, + "reference": { + "anyOf": [ + { + "$ref": "#/definitions/HashedUri" + }, + { + "type": "null" + } + ] + }, + "regionOfInterest": { + "anyOf": [ + { + "$ref": "#/definitions/RegionOfInterest" + }, + { + "type": "null" + } + ] + }, + "reviewRatings": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ReviewRating" + } + } + } + }, + "AssetType": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] + } + } + }, + "ClaimGeneratorInfo": { + "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", + "type": "object", + "required": [ + "name" + ], + "properties": { + "icon": { + "description": "hashed URI to the icon (either embedded or remote)", + "anyOf": [ + { + "$ref": "#/definitions/UriOrResource" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A human readable string naming the claim_generator", + "type": "string" + }, + "operating_system": { + "description": "A human readable string of the OS the claim generator is running on", + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "A human readable string of the product's version", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": true + }, + "Coordinate": { + "description": "An x, y coordinate used for specifying vertices in polygons.", + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "description": "The coordinate along the x-axis.", + "type": "number", + "format": "double" + }, + "y": { + "description": "The coordinate along the y-axis.", + "type": "number", + "format": "double" + } + } + }, + "DataSource": { + "description": "A description of the source for assertion data", + "type": "object", + "required": [ + "type" + ], + "properties": { + "actors": { + "description": "A list of [`Actor`]s associated with this source.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Actor" + } + }, + "details": { + "description": "A human-readable string giving details about the source of the assertion data.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "A value from among the enumerated list indicating the source of the assertion.", + "type": "string" + } + } + }, + "DateT": { + "type": "string" + }, + "Frame": { + "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", + "type": "object", + "properties": { + "end": { + "description": "The end of the frame inclusive or the end of the asset if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "start": { + "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "HashedUri": { + "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", + "type": "object", + "required": [ + "hash", + "url" + ], + "properties": { + "alg": { + "description": "A string identifying the cryptographic hash algorithm used to compute the hash", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "Byte string containing the hash value", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "url": { + "description": "JUMBF URI reference", + "type": "string" + } + } + }, + "Ingredient": { + "description": "An `Ingredient` is any external asset that has been used in the creation of an asset.", + "type": "object", + "properties": { + "active_manifest": { + "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", + "type": [ + "string", + "null" + ] + }, + "data": { + "description": "A reference to the actual data of the ingredient.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "data_types": { + "description": "Additional information about the data's type to the ingredient V2 structure.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "description": { + "description": "Additional description of the ingredient.", + "type": [ + "string", + "null" + ] + }, + "document_id": { + "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "format": { + "description": "The format of the source file as a MIME type.", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "An optional hash of the asset to prevent duplicates.", + "type": [ + "string", + "null" + ] + }, + "informational_URI": { + "description": "URI to an informational page about the ingredient or its data.", + "type": [ + "string", + "null" + ] + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "label": { + "description": "The ingredient's label as assigned in the manifest.", + "type": [ + "string", + "null" + ] + }, + "manifest_data": { + "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Any additional [`Metadata`] as defined in the C2PA spec.\n\n[`Metadata`]: crate::Metadata", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "provenance": { + "description": "URI from `dcterms:provenance` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "relationship": { + "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", + "default": "componentOf", + "allOf": [ + { + "$ref": "#/definitions/Relationship" + } + ] + }, + "resources": { + "readOnly": true, + "allOf": [ + { + "$ref": "#/definitions/ResourceStore" + } + ] + }, + "thumbnail": { + "description": "A thumbnail image capturing the visual state at the time of import.\n\nA tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "validation_results": { + "description": "Validation results (Ingredient.V3)", + "anyOf": [ + { + "$ref": "#/definitions/ValidationResults" + }, + { + "type": "null" + } + ] + }, + "validation_status": { + "description": "Validation status (Ingredient v1 & v2)", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "IngredientDeltaValidationResult": { + "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", + "type": "object", + "required": [ + "ingredientAssertionURI", + "validationDeltas" + ], + "properties": { + "ingredientAssertionURI": { + "description": "JUMBF URI reference to the ingredient assertion", + "type": "string" + }, + "validationDeltas": { + "description": "Validation results for the ingredient's active manifest", + "allOf": [ + { + "$ref": "#/definitions/StatusCodes" + } + ] + } + } + }, + "Item": { + "description": "Description of the boundaries of an identified range.", + "type": "object", + "required": [ + "identifier", + "value" + ], + "properties": { + "identifier": { + "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", + "type": "string" + }, + "value": { + "description": "The value of the identifier, e.g. a value of \"2\" for an identifier of \"track_id\" would imply track 2 of the asset.", + "type": "string" + } + } + }, + "Range": { + "description": "A spatial, temporal, frame, or textual range describing the region of interest.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "frame": { + "description": "A frame range.", + "anyOf": [ + { + "$ref": "#/definitions/Frame" + }, + { + "type": "null" + } + ] + }, + "item": { + "description": "A item identifier.", + "anyOf": [ + { + "$ref": "#/definitions/Item" + }, + { + "type": "null" + } + ] + }, + "shape": { + "description": "A spatial range.", + "anyOf": [ + { + "$ref": "#/definitions/Shape" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "A textual range.", + "anyOf": [ + { + "$ref": "#/definitions/Text" + }, + { + "type": "null" + } + ] + }, + "time": { + "description": "A temporal range.", + "anyOf": [ + { + "$ref": "#/definitions/Time" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "The type of range of interest.", + "allOf": [ + { + "$ref": "#/definitions/RangeType" + } + ] + } + } + }, + "RangeType": { + "description": "The type of range for the region of interest.", + "oneOf": [ + { + "description": "A spatial range, see [`Shape`] for more details.", + "type": "string", + "enum": [ + "spatial" + ] + }, + { + "description": "A temporal range, see [`Time`] for more details.", + "type": "string", + "enum": [ + "temporal" + ] + }, + { + "description": "A spatial range, see [`Frame`] for more details.", + "type": "string", + "enum": [ + "frame" + ] + }, + { + "description": "A textual range, see [`Text`] for more details.", + "type": "string", + "enum": [ + "textual" + ] + }, + { + "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", + "type": "string", + "enum": [ + "identified" + ] + } + ] + }, + "RegionOfInterest": { + "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", + "type": "object", + "required": [ + "region" + ], + "properties": { + "description": { + "description": "A free-text string.", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "Additional information about the asset.", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", + "type": [ + "string", + "null" + ] + }, + "region": { + "description": "A range describing the region of interest for the specific asset.", + "type": "array", + "items": { + "$ref": "#/definitions/Range" + } + }, + "role": { + "description": "A value from our controlled vocabulary or an entity-specific value (e.g., com.litware.coolArea) that represents the role of a region among other regions.", + "anyOf": [ + { + "$ref": "#/definitions/Role" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", + "type": [ + "string", + "null" + ] + } + } + }, + "Relationship": { + "description": "The relationship of the ingredient to the current asset.", + "oneOf": [ + { + "description": "The current asset is derived from this ingredient.", + "type": "string", + "enum": [ + "parentOf" + ] + }, + { + "description": "The current asset is a part of this ingredient.", + "type": "string", + "enum": [ + "componentOf" + ] + }, + { + "description": "The ingredient was used as an input to a computational process to create or modify the asset.", + "type": "string", + "enum": [ + "inputTo" + ] + } + ] + }, + "ResourceRef": { + "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", + "type": "object", + "required": [ + "format", + "identifier" + ], + "properties": { + "alg": { + "description": "The algorithm used to hash the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "data_types": { + "description": "More detailed data types as defined in the C2PA spec.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "format": { + "description": "The mime type of the referenced resource.", + "type": "string" + }, + "hash": { + "description": "The hash of the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", + "type": "string" + } + } + }, + "ResourceStore": { + "description": "Resource store to contain binary objects referenced from JSON serializable structures", + "type": "object", + "required": [ + "resources" + ], + "properties": { + "base_path": { + "type": [ + "string", + "null" + ] + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "resources": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + } + } + }, + "ReviewRating": { + "description": "A rating on an Assertion.\n\nSee .", + "type": "object", + "required": [ + "explanation", + "value" + ], + "properties": { + "code": { + "type": [ + "string", + "null" + ] + }, + "explanation": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "Role": { + "description": "A role describing the region.", + "oneOf": [ + { + "description": "Arbitrary area worth identifying.", + "type": "string", + "enum": [ + "c2pa.areaOfInterest" + ] + }, + { + "description": "This area is all that is left after a crop action.", + "type": "string", + "enum": [ + "c2pa.cropped" + ] + }, + { + "description": "This area has had edits applied to it.", + "type": "string", + "enum": [ + "c2pa.edited" + ] + }, + { + "description": "The area where an ingredient was placed/added.", + "type": "string", + "enum": [ + "c2pa.placed" + ] + }, + { + "description": "Something in this area was redacted.", + "type": "string", + "enum": [ + "c2pa.redacted" + ] + }, + { + "description": "Area specific to a subject (human or not).", + "type": "string", + "enum": [ + "c2pa.subjectArea" + ] + }, + { + "description": "A range of information was removed/deleted.", + "type": "string", + "enum": [ + "c2pa.deleted" + ] + }, + { + "description": "Styling was applied to this area.", + "type": "string", + "enum": [ + "c2pa.styled" + ] + }, + { + "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", + "type": "string", + "enum": [ + "c2pa.watermarked" + ] + } + ] + }, + "Shape": { + "description": "A spatial range representing rectangle, circle, or a polygon.", + "type": "object", + "required": [ + "origin", + "type", + "unit" + ], + "properties": { + "height": { + "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "inside": { + "description": "If the range is inside the shape.\n\nThe default value is true.", + "type": [ + "boolean", + "null" + ] + }, + "origin": { + "description": "THe origin of the coordinate in the shape.", + "allOf": [ + { + "$ref": "#/definitions/Coordinate" + } + ] + }, + "type": { + "description": "The type of shape.", + "allOf": [ + { + "$ref": "#/definitions/ShapeType" + } + ] + }, + "unit": { + "description": "The type of unit for the shape range.", + "allOf": [ + { + "$ref": "#/definitions/UnitType" + } + ] + }, + "vertices": { + "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Coordinate" + } + }, + "width": { + "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + } + } + }, + "ShapeType": { + "description": "The type of shape for the range.", + "oneOf": [ + { + "description": "A rectangle.", + "type": "string", + "enum": [ + "rectangle" + ] + }, + { + "description": "A circle.", + "type": "string", + "enum": [ + "circle" + ] + }, + { + "description": "A polygon.", + "type": "string", + "enum": [ + "polygon" + ] + } + ] + }, + "StatusCodes": { + "description": "Contains a set of success, informational, and failure validation status codes.", + "type": "object", + "required": [ + "failure", + "informational", + "success" + ], + "properties": { + "failure": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "informational": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "success": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "Text": { + "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", + "type": "object", + "required": [ + "selectors" + ], + "properties": { + "selectors": { + "description": "The ranges of text to select.", + "type": "array", + "items": { + "$ref": "#/definitions/TextSelectorRange" + } + } + } + }, + "TextSelector": { + "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", + "type": "object", + "required": [ + "fragment" + ], + "properties": { + "end": { + "description": "The end character offset or the end of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "fragment": { + "description": "Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O.", + "type": "string" + }, + "start": { + "description": "The start character offset or the start of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "TextSelectorRange": { + "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", + "type": "object", + "required": [ + "selector" + ], + "properties": { + "end": { + "description": "The end of the text range.", + "anyOf": [ + { + "$ref": "#/definitions/TextSelector" + }, + { + "type": "null" + } + ] + }, + "selector": { + "description": "The start (or entire) text range.", + "allOf": [ + { + "$ref": "#/definitions/TextSelector" + } + ] + } + } + }, + "Time": { + "description": "A temporal range representing a starting time to an ending time.", + "type": "object", + "properties": { + "end": { + "description": "The end time or the end of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "start": { + "description": "The start time or the start of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "The type of time.", + "default": "npt", + "allOf": [ + { + "$ref": "#/definitions/TimeType" + } + ] + } + } + }, + "TimeType": { + "description": "The type of time.", + "oneOf": [ + { + "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", + "type": "string", + "enum": [ + "npt" + ] + } + ] + }, + "UnitType": { + "description": "The type of unit for the range.", + "oneOf": [ + { + "description": "Use pixels.", + "type": "string", + "enum": [ + "pixel" + ] + }, + { + "description": "Use percentage.", + "type": "string", + "enum": [ + "percent" + ] + } + ] + }, + "UriOrResource": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "$ref": "#/definitions/HashedUri" + } + ] + }, + "ValidationResults": { + "description": "A map of validation results for a manifest store.\n\nThe map contains the validation results for the active manifest and any ingredient deltas. It is normal for there to be many", + "type": "object", + "properties": { + "activeManifest": { + "anyOf": [ + { + "$ref": "#/definitions/StatusCodes" + }, + { + "type": "null" + } + ] + }, + "ingredientDeltas": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/IngredientDeltaValidationResult" + } + } + } + }, + "ValidationStatus": { + "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + }, + "explanation": { + "type": [ + "string", + "null" + ] + }, + "success": { + "writeOnly": true, + "type": [ + "boolean", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + } + } + } + } +} \ No newline at end of file diff --git a/pkg/c2pa/Reader.schema.json b/pkg/c2pa/Reader.schema.json new file mode 100644 index 00000000..b481d0a2 --- /dev/null +++ b/pkg/c2pa/Reader.schema.json @@ -0,0 +1,1524 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Reader", + "description": "Use a Reader to read and validate a manifest store.", + "type": "object", + "required": [ + "manifests" + ], + "properties": { + "active_manifest": { + "description": "A label for the active (most recent) manifest in the store", + "type": [ + "string", + "null" + ] + }, + "manifests": { + "description": "A HashMap of Manifests", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Manifest" + } + }, + "validation_results": { + "description": "ValidationStatus generated when loading the ManifestStore from an asset", + "anyOf": [ + { + "$ref": "#/definitions/ValidationResults" + }, + { + "type": "null" + } + ] + }, + "validation_state": { + "description": "The validation state of the manifest store", + "anyOf": [ + { + "$ref": "#/definitions/ValidationState" + }, + { + "type": "null" + } + ] + }, + "validation_status": { + "description": "ValidationStatus generated when loading the ManifestStore from an asset", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + }, + "definitions": { + "Actor": { + "description": "Identifies a person responsible for an action.", + "type": "object", + "properties": { + "credentials": { + "description": "List of references to W3C Verifiable Credentials.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HashedUri" + } + }, + "identifier": { + "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", + "type": [ + "string", + "null" + ] + } + } + }, + "AssertionMetadata": { + "description": "The AssertionMetadata structure can be used as part of other assertions or on its own to reference others", + "type": "object", + "properties": { + "dataSource": { + "anyOf": [ + { + "$ref": "#/definitions/DataSource" + }, + { + "type": "null" + } + ] + }, + "dateTime": { + "anyOf": [ + { + "$ref": "#/definitions/DateT" + }, + { + "type": "null" + } + ] + }, + "reference": { + "anyOf": [ + { + "$ref": "#/definitions/HashedUri" + }, + { + "type": "null" + } + ] + }, + "regionOfInterest": { + "anyOf": [ + { + "$ref": "#/definitions/RegionOfInterest" + }, + { + "type": "null" + } + ] + }, + "reviewRatings": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ReviewRating" + } + } + } + }, + "AssetType": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": [ + "string", + "null" + ] + } + } + }, + "ClaimGeneratorInfo": { + "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", + "type": "object", + "required": [ + "name" + ], + "properties": { + "icon": { + "description": "hashed URI to the icon (either embedded or remote)", + "anyOf": [ + { + "$ref": "#/definitions/UriOrResource" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A human readable string naming the claim_generator", + "type": "string" + }, + "operating_system": { + "description": "A human readable string of the OS the claim generator is running on", + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "A human readable string of the product's version", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": true + }, + "Coordinate": { + "description": "An x, y coordinate used for specifying vertices in polygons.", + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "description": "The coordinate along the x-axis.", + "type": "number", + "format": "double" + }, + "y": { + "description": "The coordinate along the y-axis.", + "type": "number", + "format": "double" + } + } + }, + "DataSource": { + "description": "A description of the source for assertion data", + "type": "object", + "required": [ + "type" + ], + "properties": { + "actors": { + "description": "A list of [`Actor`]s associated with this source.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Actor" + } + }, + "details": { + "description": "A human-readable string giving details about the source of the assertion data.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "A value from among the enumerated list indicating the source of the assertion.", + "type": "string" + } + } + }, + "DateT": { + "type": "string" + }, + "Frame": { + "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", + "type": "object", + "properties": { + "end": { + "description": "The end of the frame inclusive or the end of the asset if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "start": { + "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "HashedUri": { + "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", + "type": "object", + "required": [ + "hash", + "url" + ], + "properties": { + "alg": { + "description": "A string identifying the cryptographic hash algorithm used to compute the hash", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "Byte string containing the hash value", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + }, + "url": { + "description": "JUMBF URI reference", + "type": "string" + } + } + }, + "Ingredient": { + "description": "An `Ingredient` is any external asset that has been used in the creation of an asset.", + "type": "object", + "properties": { + "active_manifest": { + "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", + "type": [ + "string", + "null" + ] + }, + "data": { + "description": "A reference to the actual data of the ingredient.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "data_types": { + "description": "Additional information about the data's type to the ingredient V2 structure.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "description": { + "description": "Additional description of the ingredient.", + "type": [ + "string", + "null" + ] + }, + "document_id": { + "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "format": { + "description": "The format of the source file as a MIME type.", + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "An optional hash of the asset to prevent duplicates.", + "type": [ + "string", + "null" + ] + }, + "informational_URI": { + "description": "URI to an informational page about the ingredient or its data.", + "type": [ + "string", + "null" + ] + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "label": { + "description": "The ingredient's label as assigned in the manifest.", + "type": [ + "string", + "null" + ] + }, + "manifest_data": { + "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Any additional [`Metadata`] as defined in the C2PA spec.\n\n[`Metadata`]: crate::Metadata", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "provenance": { + "description": "URI from `dcterms:provenance` in XMP metadata.", + "type": [ + "string", + "null" + ] + }, + "relationship": { + "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", + "default": "componentOf", + "allOf": [ + { + "$ref": "#/definitions/Relationship" + } + ] + }, + "resources": { + "readOnly": true, + "allOf": [ + { + "$ref": "#/definitions/ResourceStore" + } + ] + }, + "thumbnail": { + "description": "A thumbnail image capturing the visual state at the time of import.\n\nA tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "validation_results": { + "description": "Validation results (Ingredient.V3)", + "anyOf": [ + { + "$ref": "#/definitions/ValidationResults" + }, + { + "type": "null" + } + ] + }, + "validation_status": { + "description": "Validation status (Ingredient v1 & v2)", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "IngredientDeltaValidationResult": { + "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", + "type": "object", + "required": [ + "ingredientAssertionURI", + "validationDeltas" + ], + "properties": { + "ingredientAssertionURI": { + "description": "JUMBF URI reference to the ingredient assertion", + "type": "string" + }, + "validationDeltas": { + "description": "Validation results for the ingredient's active manifest", + "allOf": [ + { + "$ref": "#/definitions/StatusCodes" + } + ] + } + } + }, + "Item": { + "description": "Description of the boundaries of an identified range.", + "type": "object", + "required": [ + "identifier", + "value" + ], + "properties": { + "identifier": { + "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", + "type": "string" + }, + "value": { + "description": "The value of the identifier, e.g. a value of \"2\" for an identifier of \"track_id\" would imply track 2 of the asset.", + "type": "string" + } + } + }, + "Manifest": { + "description": "A Manifest represents all the information in a c2pa manifest", + "type": "object", + "properties": { + "assertions": { + "description": "A list of assertions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ManifestAssertion" + } + }, + "claim_generator": { + "description": "A User Agent formatted string identifying the software/hardware/system produced this claim Spaces are not allowed in names, versions can be specified with product/1.0 syntax.", + "type": [ + "string", + "null" + ] + }, + "claim_generator_info": { + "description": "A list of claim generator info data identifying the software/hardware/system produced this claim.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ClaimGeneratorInfo" + } + }, + "credentials": { + "description": "A List of verified credentials", + "type": [ + "array", + "null" + ], + "items": true + }, + "format": { + "description": "The format of the source file as a MIME type.", + "type": [ + "string", + "null" + ] + }, + "ingredients": { + "description": "A List of ingredients", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Ingredient" + } + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "default": "xmp:iid:855e296c-2f54-4a0a-ae5a-0659b3bca8b8", + "type": "string" + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "A list of user metadata for this claim.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssertionMetadata" + } + }, + "redactions": { + "description": "A list of redactions - URIs to a redacted assertions", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "resources": { + "description": "container for binary assets (like thumbnails)", + "readOnly": true, + "allOf": [ + { + "$ref": "#/definitions/ResourceStore" + } + ] + }, + "signature_info": { + "description": "Signature data (only used for reporting)", + "anyOf": [ + { + "$ref": "#/definitions/SignatureInfo" + }, + { + "type": "null" + } + ] + }, + "thumbnail": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": [ + "string", + "null" + ] + }, + "vendor": { + "description": "Optional prefix added to the generated Manifest label. This is typically an internet domain name for the vendor (i.e. `adobe`).", + "type": [ + "string", + "null" + ] + } + } + }, + "ManifestAssertion": { + "description": "A labeled container for an Assertion value in a Manifest", + "type": "object", + "required": [ + "data", + "label" + ], + "properties": { + "data": { + "description": "The data of the assertion as Value", + "allOf": [ + { + "$ref": "#/definitions/ManifestData" + } + ] + }, + "instance": { + "description": "There can be more than one assertion for any label", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "kind": { + "description": "The [ManifestAssertionKind] for this assertion (as stored in c2pa content)", + "anyOf": [ + { + "$ref": "#/definitions/ManifestAssertionKind" + }, + { + "type": "null" + } + ] + }, + "label": { + "description": "An assertion label in reverse domain format", + "type": "string" + } + } + }, + "ManifestAssertionKind": { + "description": "Assertions in C2PA can be stored in several formats", + "type": "string", + "enum": [ + "Cbor", + "Json", + "Binary", + "Uri" + ] + }, + "ManifestData": { + "anyOf": [ + true, + { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + ] + }, + "Range": { + "description": "A spatial, temporal, frame, or textual range describing the region of interest.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "frame": { + "description": "A frame range.", + "anyOf": [ + { + "$ref": "#/definitions/Frame" + }, + { + "type": "null" + } + ] + }, + "item": { + "description": "A item identifier.", + "anyOf": [ + { + "$ref": "#/definitions/Item" + }, + { + "type": "null" + } + ] + }, + "shape": { + "description": "A spatial range.", + "anyOf": [ + { + "$ref": "#/definitions/Shape" + }, + { + "type": "null" + } + ] + }, + "text": { + "description": "A textual range.", + "anyOf": [ + { + "$ref": "#/definitions/Text" + }, + { + "type": "null" + } + ] + }, + "time": { + "description": "A temporal range.", + "anyOf": [ + { + "$ref": "#/definitions/Time" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "The type of range of interest.", + "allOf": [ + { + "$ref": "#/definitions/RangeType" + } + ] + } + } + }, + "RangeType": { + "description": "The type of range for the region of interest.", + "oneOf": [ + { + "description": "A spatial range, see [`Shape`] for more details.", + "type": "string", + "enum": [ + "spatial" + ] + }, + { + "description": "A temporal range, see [`Time`] for more details.", + "type": "string", + "enum": [ + "temporal" + ] + }, + { + "description": "A spatial range, see [`Frame`] for more details.", + "type": "string", + "enum": [ + "frame" + ] + }, + { + "description": "A textual range, see [`Text`] for more details.", + "type": "string", + "enum": [ + "textual" + ] + }, + { + "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", + "type": "string", + "enum": [ + "identified" + ] + } + ] + }, + "RegionOfInterest": { + "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", + "type": "object", + "required": [ + "region" + ], + "properties": { + "description": { + "description": "A free-text string.", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", + "type": [ + "string", + "null" + ] + }, + "metadata": { + "description": "Additional information about the asset.", + "anyOf": [ + { + "$ref": "#/definitions/AssertionMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", + "type": [ + "string", + "null" + ] + }, + "region": { + "description": "A range describing the region of interest for the specific asset.", + "type": "array", + "items": { + "$ref": "#/definitions/Range" + } + }, + "role": { + "description": "A value from our controlled vocabulary or an entity-specific value (e.g., com.litware.coolArea) that represents the role of a region among other regions.", + "anyOf": [ + { + "$ref": "#/definitions/Role" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", + "type": [ + "string", + "null" + ] + } + } + }, + "Relationship": { + "description": "The relationship of the ingredient to the current asset.", + "oneOf": [ + { + "description": "The current asset is derived from this ingredient.", + "type": "string", + "enum": [ + "parentOf" + ] + }, + { + "description": "The current asset is a part of this ingredient.", + "type": "string", + "enum": [ + "componentOf" + ] + }, + { + "description": "The ingredient was used as an input to a computational process to create or modify the asset.", + "type": "string", + "enum": [ + "inputTo" + ] + } + ] + }, + "ResourceRef": { + "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", + "type": "object", + "required": [ + "format", + "identifier" + ], + "properties": { + "alg": { + "description": "The algorithm used to hash the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "data_types": { + "description": "More detailed data types as defined in the C2PA spec.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AssetType" + } + }, + "format": { + "description": "The mime type of the referenced resource.", + "type": "string" + }, + "hash": { + "description": "The hash of the resource (if applicable).", + "type": [ + "string", + "null" + ] + }, + "identifier": { + "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", + "type": "string" + } + } + }, + "ResourceStore": { + "description": "Resource store to contain binary objects referenced from JSON serializable structures", + "type": "object", + "required": [ + "resources" + ], + "properties": { + "base_path": { + "type": [ + "string", + "null" + ] + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "resources": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + } + } + }, + "ReviewRating": { + "description": "A rating on an Assertion.\n\nSee .", + "type": "object", + "required": [ + "explanation", + "value" + ], + "properties": { + "code": { + "type": [ + "string", + "null" + ] + }, + "explanation": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + } + }, + "Role": { + "description": "A role describing the region.", + "oneOf": [ + { + "description": "Arbitrary area worth identifying.", + "type": "string", + "enum": [ + "c2pa.areaOfInterest" + ] + }, + { + "description": "This area is all that is left after a crop action.", + "type": "string", + "enum": [ + "c2pa.cropped" + ] + }, + { + "description": "This area has had edits applied to it.", + "type": "string", + "enum": [ + "c2pa.edited" + ] + }, + { + "description": "The area where an ingredient was placed/added.", + "type": "string", + "enum": [ + "c2pa.placed" + ] + }, + { + "description": "Something in this area was redacted.", + "type": "string", + "enum": [ + "c2pa.redacted" + ] + }, + { + "description": "Area specific to a subject (human or not).", + "type": "string", + "enum": [ + "c2pa.subjectArea" + ] + }, + { + "description": "A range of information was removed/deleted.", + "type": "string", + "enum": [ + "c2pa.deleted" + ] + }, + { + "description": "Styling was applied to this area.", + "type": "string", + "enum": [ + "c2pa.styled" + ] + }, + { + "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", + "type": "string", + "enum": [ + "c2pa.watermarked" + ] + } + ] + }, + "Shape": { + "description": "A spatial range representing rectangle, circle, or a polygon.", + "type": "object", + "required": [ + "origin", + "type", + "unit" + ], + "properties": { + "height": { + "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + }, + "inside": { + "description": "If the range is inside the shape.\n\nThe default value is true.", + "type": [ + "boolean", + "null" + ] + }, + "origin": { + "description": "THe origin of the coordinate in the shape.", + "allOf": [ + { + "$ref": "#/definitions/Coordinate" + } + ] + }, + "type": { + "description": "The type of shape.", + "allOf": [ + { + "$ref": "#/definitions/ShapeType" + } + ] + }, + "unit": { + "description": "The type of unit for the shape range.", + "allOf": [ + { + "$ref": "#/definitions/UnitType" + } + ] + }, + "vertices": { + "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Coordinate" + } + }, + "width": { + "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", + "type": [ + "number", + "null" + ], + "format": "double" + } + } + }, + "ShapeType": { + "description": "The type of shape for the range.", + "oneOf": [ + { + "description": "A rectangle.", + "type": "string", + "enum": [ + "rectangle" + ] + }, + { + "description": "A circle.", + "type": "string", + "enum": [ + "circle" + ] + }, + { + "description": "A polygon.", + "type": "string", + "enum": [ + "polygon" + ] + } + ] + }, + "SignatureInfo": { + "description": "Holds information about a signature", + "type": "object", + "properties": { + "alg": { + "description": "Human-readable issuing authority for this signature.", + "anyOf": [ + { + "$ref": "#/definitions/SigningAlg" + }, + { + "type": "null" + } + ] + }, + "cert_serial_number": { + "description": "The serial number of the certificate.", + "type": [ + "string", + "null" + ] + }, + "issuer": { + "description": "Human-readable issuing authority for this signature.", + "type": [ + "string", + "null" + ] + }, + "revocation_status": { + "description": "Revocation status of the certificate.", + "type": [ + "boolean", + "null" + ] + }, + "time": { + "description": "The time the signature was created.", + "type": [ + "string", + "null" + ] + } + } + }, + "SigningAlg": { + "description": "Describes the digital signature algorithms allowed by the C2PA spec.\n\nPer [§13.2, “Digital Signatures”]:\n\n> All digital signatures applied as per the technical requirements of this > specification shall be generated using one of the digital signature > algorithms and key types listed as described in this section.\n\n[§13.2, “Digital Signatures”]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_digital_signatures", + "oneOf": [ + { + "description": "ECDSA with SHA-256", + "type": "string", + "enum": [ + "Es256" + ] + }, + { + "description": "ECDSA with SHA-256 on secp256k1 curve", + "type": "string", + "enum": [ + "Es256K" + ] + }, + { + "description": "ECDSA with SHA-384", + "type": "string", + "enum": [ + "Es384" + ] + }, + { + "description": "ECDSA with SHA-512", + "type": "string", + "enum": [ + "Es512" + ] + }, + { + "description": "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", + "type": "string", + "enum": [ + "Ps256" + ] + }, + { + "description": "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", + "type": "string", + "enum": [ + "Ps384" + ] + }, + { + "description": "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", + "type": "string", + "enum": [ + "Ps512" + ] + }, + { + "description": "Edwards-Curve DSA (Ed25519 instance only)", + "type": "string", + "enum": [ + "Ed25519" + ] + } + ] + }, + "StatusCodes": { + "description": "Contains a set of success, informational, and failure validation status codes.", + "type": "object", + "required": [ + "failure", + "informational", + "success" + ], + "properties": { + "failure": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "informational": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + }, + "success": { + "type": "array", + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, + "Text": { + "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", + "type": "object", + "required": [ + "selectors" + ], + "properties": { + "selectors": { + "description": "The ranges of text to select.", + "type": "array", + "items": { + "$ref": "#/definitions/TextSelectorRange" + } + } + } + }, + "TextSelector": { + "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", + "type": "object", + "required": [ + "fragment" + ], + "properties": { + "end": { + "description": "The end character offset or the end of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "fragment": { + "description": "Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O.", + "type": "string" + }, + "start": { + "description": "The start character offset or the start of the fragment if not present.", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "TextSelectorRange": { + "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", + "type": "object", + "required": [ + "selector" + ], + "properties": { + "end": { + "description": "The end of the text range.", + "anyOf": [ + { + "$ref": "#/definitions/TextSelector" + }, + { + "type": "null" + } + ] + }, + "selector": { + "description": "The start (or entire) text range.", + "allOf": [ + { + "$ref": "#/definitions/TextSelector" + } + ] + } + } + }, + "Time": { + "description": "A temporal range representing a starting time to an ending time.", + "type": "object", + "properties": { + "end": { + "description": "The end time or the end of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "start": { + "description": "The start time or the start of the asset if not present.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "The type of time.", + "default": "npt", + "allOf": [ + { + "$ref": "#/definitions/TimeType" + } + ] + } + } + }, + "TimeType": { + "description": "The type of time.", + "oneOf": [ + { + "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", + "type": "string", + "enum": [ + "npt" + ] + } + ] + }, + "UnitType": { + "description": "The type of unit for the range.", + "oneOf": [ + { + "description": "Use pixels.", + "type": "string", + "enum": [ + "pixel" + ] + }, + { + "description": "Use percentage.", + "type": "string", + "enum": [ + "percent" + ] + } + ] + }, + "UriOrResource": { + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "$ref": "#/definitions/HashedUri" + } + ] + }, + "ValidationResults": { + "description": "A map of validation results for a manifest store.\n\nThe map contains the validation results for the active manifest and any ingredient deltas. It is normal for there to be many", + "type": "object", + "properties": { + "activeManifest": { + "anyOf": [ + { + "$ref": "#/definitions/StatusCodes" + }, + { + "type": "null" + } + ] + }, + "ingredientDeltas": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/IngredientDeltaValidationResult" + } + } + } + }, + "ValidationState": { + "description": "Indicates if the manifest store is valid and trusted.\n\nThe Trusted state implies the manifest store is valid and the active signature is trusted.", + "oneOf": [ + { + "description": "Errors were found in the manifest store.", + "type": "string", + "enum": [ + "Invalid" + ] + }, + { + "description": "No errors were found in validation, but the active signature is not trusted.", + "type": "string", + "enum": [ + "Valid" + ] + }, + { + "description": "The manifest store is valid and the active signature is trusted.", + "type": "string", + "enum": [ + "Trusted" + ] + } + ] + }, + "ValidationStatus": { + "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + }, + "explanation": { + "type": [ + "string", + "null" + ] + }, + "success": { + "writeOnly": true, + "type": [ + "boolean", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + } + } + } + } +} \ No newline at end of file -- 2.51.2 From 866df53014c4925285663801f2636f0f2a37d9ae Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 00:32:14 -0500 Subject: [PATCH 009/525] fix overflowing settings on mobile --- js/app/components/settings/settings.tsx | 87 ++++++++++++------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/js/app/components/settings/settings.tsx b/js/app/components/settings/settings.tsx index 2f49346a..7cbafc55 100644 --- a/js/app/components/settings/settings.tsx +++ b/js/app/components/settings/settings.tsx @@ -51,10 +51,10 @@ export function Settings() { return ( - + @@ -65,15 +65,16 @@ export function Settings() { Use Custom Node - Default: {url} + Default: {defaultUrl} - - - {/* Custom URL Input Row */} - - - + + + + + @@ -165,7 +161,6 @@ export function Settings() { - )} -- 2.51.2 From c26d72ceafa5c75ff011bed463da502abc31152e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 01:08:37 -0500 Subject: [PATCH 010/525] add atproto proxy header to app.bsky.actor.getProfile --- pkg/spxrpc/app_bsky_actor.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/spxrpc/app_bsky_actor.go b/pkg/spxrpc/app_bsky_actor.go index 38e05455..daa0a331 100644 --- a/pkg/spxrpc/app_bsky_actor.go +++ b/pkg/spxrpc/app_bsky_actor.go @@ -16,6 +16,11 @@ func (s *Server) handleAppBskyActorGetProfile(ctx context.Context, actor string) return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") } + // in case the end user doesn't have a default fallback client in the pds + client.SetHeaders(map[string]string{ + "atproto-proxy": "did:web:api.bsky.app#bsky_appview", + }) + // brief check to make sure we can actually do stuff var out appbskytypes.ActorDefs_ProfileViewDetailed err := client.Do(ctx, xrpc.Query, "application/json", "app.bsky.actor.getProfile", map[string]any{"actor": actor}, nil, &out) -- 2.51.2 From b2e34d38c30cb16fa083f4840b5d4dc4db4ee6c0 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 13:42:22 -0500 Subject: [PATCH 011/525] capitalize to Atproto-Proxy for consistency --- pkg/spxrpc/app_bsky_actor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/spxrpc/app_bsky_actor.go b/pkg/spxrpc/app_bsky_actor.go index daa0a331..b47988e0 100644 --- a/pkg/spxrpc/app_bsky_actor.go +++ b/pkg/spxrpc/app_bsky_actor.go @@ -18,7 +18,7 @@ func (s *Server) handleAppBskyActorGetProfile(ctx context.Context, actor string) // in case the end user doesn't have a default fallback client in the pds client.SetHeaders(map[string]string{ - "atproto-proxy": "did:web:api.bsky.app#bsky_appview", + "Atproto-Proxy": "did:web:api.bsky.app#bsky_appview", }) // brief check to make sure we can actually do stuff -- 2.51.2 From 3d5dc6aff6d40c7af9a927c0db1c1de5150b9a15 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 18 Sep 2025 16:29:14 -0500 Subject: [PATCH 012/525] add better controls to desktop player --- js/app/components/mobile/ui.tsx | 238 +++++++++++++++++++++++++++----- 1 file changed, 201 insertions(+), 37 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index a1030443..f2c6256a 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -1,6 +1,7 @@ import { useNavigation } from "@react-navigation/native"; import { PlayerUI, + Slider, Text, Toast, useAvatars, @@ -11,12 +12,26 @@ import { usePlayerStore, useSegmentDimensions, useSetMuted, + useSetVolume, + useTheme, + useVolume, View, zero, } from "@streamplace/components"; -import { ChevronLeft, SwitchCamera, VolumeX } from "lucide-react-native"; +import { + ChevronLeft, + Maximize2, + SwitchCamera, + Volume2, + VolumeX, +} from "lucide-react-native"; import { useEffect, useRef, useState } from "react"; -import { Image, Pressable, TouchableWithoutFeedback } from "react-native"; +import { + Image, + Platform, + Pressable, + TouchableWithoutFeedback, +} from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -29,6 +44,7 @@ const { borders, colors, gap, h, layout, position, w, bottom, px, py, r } = zero; export function MobileUi() { + const { theme } = useTheme(); const navigation = useNavigation(); const { ingest, @@ -161,47 +177,37 @@ export function MobileUi() { - {shouldShowFloatingMetrics && ( - - - - )} - + {/* Right Controls Column */} - {ingest === null ? ( - - ) : ( - - - + {shouldShowFloatingMetrics && ( + + + + + )} + + {shouldShowFloatingMetrics && isLive && ( @@ -221,7 +227,6 @@ export function MobileUi() { )} - {isSelfAndNotLive && ( ); } + +function RightControlsPanel({ ingest, doSetIngestCamera }) { + const { colors } = zero; + const { styles, theme } = useTheme(); + const volume = useVolume(); + const setVolume = useSetVolume(); + const muted = useMuted(); + const setMuted = useSetMuted(); + const fullscreen = usePlayerStore((state) => state.fullscreen); + const setFullscreen = usePlayerStore((state) => state.setFullscreen); + + const [showVolumeSlider, setShowVolumeSlider] = useState(false); + + const handleVolumePress = () => { + setShowVolumeSlider(!showVolumeSlider); + }; + + const handleVolumeChange = (values) => { + const newVolume = values[0] / 100; // Convert from 0-100 to 0-1 + setVolume(newVolume); + if (newVolume === 0) { + setMuted(true); + } else { + setMuted(false); + } + }; + + const handleFullscreenToggle = () => { + if (Platform.OS === "web") { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch((err) => { + console.error("Fullscreen request failed:", err); + }); + } else { + document.exitFullscreen().catch((err) => { + console.error("Exit fullscreen failed:", err); + }); + } + } + }; + + const sliderValue = (muted ? 0 : volume) * 100; + + return ( + + + {ingest === null ? ( + Platform.OS === "web" && + ) : ( + + + + )} + {Platform.OS === "web" ? ( + <> + + + + + {muted || volume === 0 ? ( + + ) : ( + + )} + + + ) : ( + + )} + + {/* Volume Slider Popup */} + {showVolumeSlider && ( + e.stopPropagation()} + onTouchMove={(e) => e.stopPropagation()} + onTouchEnd={(e) => e.stopPropagation()} + > + + + + + + + + )} + + ); +} -- 2.51.2 From 40c32b3fc86c928328bbbefec9908fe84fb3160d Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 18 Sep 2025 16:29:46 -0500 Subject: [PATCH 013/525] top bar gone :0 --- js/app/components/mobile/chat.tsx | 2 -- js/app/components/mobile/player.tsx | 5 ++++ js/app/components/mobile/ui.tsx | 44 +++++++++++++---------------- js/app/src/router.tsx | 1 + 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/js/app/components/mobile/chat.tsx b/js/app/components/mobile/chat.tsx index e5dc7f1f..2268bffc 100644 --- a/js/app/components/mobile/chat.tsx +++ b/js/app/components/mobile/chat.tsx @@ -17,7 +17,6 @@ import Animated, { useSharedValue, withSpring, } from "react-native-reanimated"; -import { useResponsiveLayout } from "./useResponsiveLayout"; import { useNavigation } from "@react-navigation/native"; import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; @@ -100,7 +99,6 @@ export function MobileChatPanel({ isPlayerRatioGreater }) { } function ChatPanel() { - const { shouldShowChatSidePanel, safeAreaInsets } = useResponsiveLayout(); const { profile } = useLivestreamInfo(); const handle = useHandle(); diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 4d9153ee..47709df1 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -10,6 +10,7 @@ import { Text, usePlayerDimensions, usePlayerStore, + useSegmentDimensions, View, } from "@streamplace/components"; import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; @@ -161,6 +162,7 @@ export function PlayerInner( screenWidth, contentWidth, availableHeight, + safeAreaInsets, } = useResponsiveLayout({ sidebarWidth: sb.animatedWidth, sidebarHidden: !sb.isActive, @@ -170,6 +172,8 @@ export function PlayerInner( // content info const { width, height } = usePlayerDimensions(); + const { isPlayerRatioGreater } = useSegmentDimensions(); + // Calculate aspect ratio and determine if we're in desktop mode const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; const isDesktopMode = shouldShowChatSidePanel || screenWidth > 768; @@ -220,6 +224,7 @@ export function PlayerInner( : { flex: 1, }, + { paddingTop: isPlayerRatioGreater ? safeAreaInsets.top : 0 }, ]} > diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index f2c6256a..a2b0244d 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -150,30 +150,26 @@ export function MobileUi() { > - {shouldShowFloatingMetrics && ( - <> - - {profile?.handle} - - )} + + {profile?.handle} diff --git a/js/app/src/router.tsx b/js/app/src/router.tsx index e54dfcab..5b18a553 100644 --- a/js/app/src/router.tsx +++ b/js/app/src/router.tsx @@ -656,6 +656,7 @@ const MainTab = () => { options={{ headerTitle: "Stream", title: "Streamplace Stream", + headerShown: false, }} /> -- 2.51.2 From e6b723ed076992f6f748d37f667ee8c45980b38b Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 18 Sep 2025 17:30:45 -0500 Subject: [PATCH 014/525] fix mobile horizontal view too --- js/app/components/mobile/chat.tsx | 2 +- js/app/components/mobile/player.tsx | 31 ++++++--- js/app/components/mobile/ui.tsx | 68 ++++++++++++++++--- .../mobile-player/ui/viewer-context-menu.tsx | 4 +- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/js/app/components/mobile/chat.tsx b/js/app/components/mobile/chat.tsx index 2268bffc..a27b23da 100644 --- a/js/app/components/mobile/chat.tsx +++ b/js/app/components/mobile/chat.tsx @@ -38,7 +38,7 @@ export function DesktopChatPanel({ "Setting sidebar offset x to", chatVisible ? 0 : chatPanelWidth, ); - sidebarOffset.value = withSpring(chatVisible ? 0 : chatPanelWidth, { + sidebarOffset.value = withSpring(chatVisible ? 0 : chatPanelWidth + 64, { damping: 100, stiffness: 1000, }); diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 47709df1..0d450f5d 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -190,11 +190,12 @@ export function PlayerInner( ? Math.min(calculatedWidth / aspectRatio, maxDesktopHeight) : height; - const showBottomMetaPanel = aspectRatio > 1 && screenWidth > 980; + const showFullDesktopMode = aspectRatio > 1 && screenWidth > 980; + const isLandscape = aspectRatio > 1; // i don't really like this, but it's the only way to ensure the // player is sized correctly on both desktop and mobile views - const ContainerElement = showBottomMetaPanel ? ScrollView : View; + const ContainerElement = showFullDesktopMode ? ScrollView : View; return ( - {(showBottomMetaPanel || fullscreen) && ( - + {showFullDesktopMode || fullscreen ? ( + + ) : ( + isLandscape && ( + + ) )} @@ -250,7 +259,7 @@ export function PlayerInner( /> - {showBottomMetaPanel && ( + {showFullDesktopMode && ( void; + showChat?: boolean; +}) { const { theme } = useTheme(); const navigation = useNavigation(); const { @@ -69,9 +76,20 @@ export function MobileUi() { const muted = useMuted(); const setMuted = useSetMuted(); - const { shouldShowFloatingMetrics, safeAreaInsets } = useResponsiveLayout(); + const { + shouldShowFloatingMetrics, + shouldShowChatSidePanel, + chatPanelWidth, + safeAreaInsets, + } = useResponsiveLayout(); const [showLoading, setShowLoading] = useState(false); + // get width/height + // showchat is a proxy for if we're in landscape or not :-( + if (showChat != undefined) { + safeAreaInsets.top = 0; + } + useEffect(() => { return () => { if (ingestStarting) { @@ -107,7 +125,10 @@ export function MobileUi() { }, []); const animatedFadeStyle = useAnimatedStyle(() => ({ - opacity: shouldShowFloatingMetrics ? 1 : fadeOpacity.value, + opacity: + shouldShowFloatingMetrics || shouldShowChatSidePanel + ? 1 + : fadeOpacity.value, })); return ( @@ -203,6 +224,9 @@ export function MobileUi() { @@ -260,7 +284,7 @@ export function MobileUi() { - {!isSelfAndNotLive && ( + {showChat === undefined && ( )} {muted && ( @@ -314,15 +338,24 @@ export function MobileUi() { ); } -function RightControlsPanel({ ingest, doSetIngestCamera }) { - const { colors } = zero; - const { styles, theme } = useTheme(); +function RightControlsPanel({ + ingest, + doSetIngestCamera, + shouldShowChatSidePanel, + showChat, + setShowChat, +}: { + ingest: string | null; + doSetIngestCamera: () => void; + shouldShowChatSidePanel: boolean; + showChat?: boolean; + setShowChat?: (show: boolean) => void; +}) { + const { theme } = useTheme(); const volume = useVolume(); const setVolume = useSetVolume(); const muted = useMuted(); const setMuted = useSetMuted(); - const fullscreen = usePlayerStore((state) => state.fullscreen); - const setFullscreen = usePlayerStore((state) => state.setFullscreen); const [showVolumeSlider, setShowVolumeSlider] = useState(false); @@ -372,7 +405,9 @@ function RightControlsPanel({ ingest, doSetIngestCamera }) { borderRadius: 12, }, zero.r[2], - zero.layout.flex.column, + showChat === undefined + ? zero.layout.flex.column + : zero.layout.flex.row, zero.layout.flex.center, zero.gap.all[4], zero.layout.position.relative, @@ -401,6 +436,19 @@ function RightControlsPanel({ ingest, doSetIngestCamera }) { ) : ( )} + {shouldShowChatSidePanel && setShowChat && ( + { + setShowChat(!showChat); + }} + > + {showChat ? ( + + ) : ( + + )} + + )} {/* Volume Slider Popup */} {showVolumeSlider && ( diff --git a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx index 162d2435..d5463cc5 100644 --- a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx +++ b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx @@ -1,5 +1,5 @@ import { useRootContext } from "@rn-primitives/dropdown-menu"; -import { Settings } from "lucide-react-native"; +import { Menu } from "lucide-react-native"; import { Platform, View } from "react-native"; import { colors } from "../../../lib/theme"; import { useLivestreamStore } from "../../../livestream-store"; @@ -57,7 +57,7 @@ export function ContextMenu({ return ( - + -- 2.51.2 From 1f5c668f45425deee69ccca92509c0ebab4a4f5e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 19 Sep 2025 10:20:21 -0500 Subject: [PATCH 015/525] revert ui changes --- js/app/components/mobile/ui.tsx | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index f24813c6..aca212cc 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -21,7 +21,8 @@ import { import { ChevronLeft, ChevronRight, - Maximize2, + Fullscreen, + Minimize, SwitchCamera, Volume2, VolumeX, @@ -357,6 +358,9 @@ function RightControlsPanel({ const muted = useMuted(); const setMuted = useSetMuted(); + const fullscreen = usePlayerStore((state) => state.fullscreen); + const setFullscreen = usePlayerStore((state) => state.setFullscreen); + const [showVolumeSlider, setShowVolumeSlider] = useState(false); const handleVolumePress = () => { @@ -373,20 +377,6 @@ function RightControlsPanel({ } }; - const handleFullscreenToggle = () => { - if (Platform.OS === "web") { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen().catch((err) => { - console.error("Fullscreen request failed:", err); - }); - } else { - document.exitFullscreen().catch((err) => { - console.error("Exit fullscreen failed:", err); - }); - } - } - }; - const sliderValue = (muted ? 0 : volume) * 100; return ( @@ -422,8 +412,14 @@ function RightControlsPanel({ )} {Platform.OS === "web" ? ( <> - - + { + // check if we can Fullscreen + if (setFullscreen) setFullscreen(!fullscreen); + }} + style={[zero.p[2], zero.r.md]} + > + {fullscreen ? : } {muted || volume === 0 ? ( -- 2.51.2 From 6c9a929c588d35494457ee44d9c5b91921edebbe Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 19 Sep 2025 12:31:33 -0500 Subject: [PATCH 016/525] clean up a lil --- js/app/components/mobile/ui.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index aca212cc..1b72d19c 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -42,8 +42,7 @@ import Animated, { import { MobileChatPanel } from "./chat"; import { useResponsiveLayout } from "./useResponsiveLayout"; -const { borders, colors, gap, h, layout, position, w, bottom, px, py, r } = - zero; +const { borders, gap, h, layout, position, w, r } = zero; export function MobileUi({ setShowChat, @@ -367,7 +366,7 @@ function RightControlsPanel({ setShowVolumeSlider(!showVolumeSlider); }; - const handleVolumeChange = (values) => { + const handleVolumeChange = (values: number[]) => { const newVolume = values[0] / 100; // Convert from 0-100 to 0-1 setVolume(newVolume); if (newVolume === 0) { -- 2.51.2 From 55bfdda2874fc7705f690147faa9a8259fd4a8f5 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 15:18:15 -0500 Subject: [PATCH 017/525] fix fullscreen reflowing on width change --- js/app/components/mobile/player.tsx | 29 ++- js/app/components/mobile/ui.tsx | 284 ++++++++++++++-------------- 2 files changed, 150 insertions(+), 163 deletions(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 0d450f5d..3ea1e63b 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -193,27 +193,20 @@ export function PlayerInner( const showFullDesktopMode = aspectRatio > 1 && screenWidth > 980; const isLandscape = aspectRatio > 1; - // i don't really like this, but it's the only way to ensure the - // player is sized correctly on both desktop and mobile views - const ContainerElement = showFullDesktopMode ? ScrollView : View; return ( - )} - + ); } diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 1b72d19c..c1cb010d 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -28,12 +28,7 @@ import { VolumeX, } from "lucide-react-native"; import { useEffect, useRef, useState } from "react"; -import { - Image, - Platform, - Pressable, - TouchableWithoutFeedback, -} from "react-native"; +import { Image, Platform, Pressable } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -133,156 +128,152 @@ export function MobileUi({ return ( <> - - + {/* Main UI Overlay */} + - {/* Main UI Overlay */} + {/* Top Left - Back Button and Profile */} - {/* Top Left - Back Button and Profile */} - - - { - navigation.canGoBack() - ? navigation.goBack() - : navigation.navigate("Home", { screen: "StreamList" }); - }} - > - - - + { + navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate("Home", { screen: "StreamList" }); + }} + > + + + + {profile?.handle} + + + + {/* Right Controls Column */} + + {shouldShowFloatingMetrics && ( + + - {profile?.handle} + > + + - + )} + + + - {/* Right Controls Column */} + {shouldShowFloatingMetrics && isLive && ( - {shouldShowFloatingMetrics && ( - - - - - - )} - - + - - {shouldShowFloatingMetrics && isLive && ( - - - - )} - - {isSelfAndNotLive && ( - )} - - { - if (!recordSubmitted && title != "") { - setShowLoading(true); - } - setShowCountdown(false); - }} - /> - + {isSelfAndNotLive && ( + + )} - - - + { + if (!recordSubmitted && title != "") { + setShowLoading(true); + } + setShowCountdown(false); + }} + /> + + + + {showChat === undefined && ( @@ -356,9 +347,8 @@ function RightControlsPanel({ const setVolume = useSetVolume(); const muted = useMuted(); const setMuted = useSetMuted(); - - const fullscreen = usePlayerStore((state) => state.fullscreen); - const setFullscreen = usePlayerStore((state) => state.setFullscreen); + const fullscreen = usePlayerStore((x) => x.fullscreen); + const setFullscreen = usePlayerStore((x) => x.setFullscreen); const [showVolumeSlider, setShowVolumeSlider] = useState(false); @@ -413,12 +403,16 @@ function RightControlsPanel({ <> { - // check if we can Fullscreen - if (setFullscreen) setFullscreen(!fullscreen); + console.log("UI.tsx Button pressed at:", Date.now()); + setFullscreen(!fullscreen); }} - style={[zero.p[2], zero.r.md]} + style={[zero.p[2], r[1]]} > - {fullscreen ? : } + {fullscreen ? ( + + ) : ( + + )} {muted || volume === 0 ? ( -- 2.51.2 From c7ba90f0ac8bf6ffb739d4788a082684eaca9b36 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 15:26:17 -0500 Subject: [PATCH 018/525] show on hover --- js/app/components/mobile/ui.tsx | 271 +++++++++++++++++--------------- 1 file changed, 142 insertions(+), 129 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index c1cb010d..3f04bbeb 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -29,6 +29,7 @@ import { } from "lucide-react-native"; import { useEffect, useRef, useState } from "react"; import { Image, Platform, Pressable } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useAnimatedStyle, useSharedValue, @@ -119,6 +120,11 @@ export function MobileUi({ }; }, []); + const onPlayerHover = () => { + "worklet"; + resetFadeTimer(); + }; + const animatedFadeStyle = useAnimatedStyle(() => ({ opacity: shouldShowFloatingMetrics || shouldShowChatSidePanel @@ -126,154 +132,161 @@ export function MobileUi({ : fadeOpacity.value, })); + const hover = Gesture.Hover().onChange(onPlayerHover); + const pan = Gesture.Pan().onChange(onPlayerHover); + + const combined = Gesture.Race(hover, pan); return ( - <> - - {/* Main UI Overlay */} - + <> + - {/* Top Left - Back Button and Profile */} + {/* Main UI Overlay */} - - { - navigation.canGoBack() - ? navigation.goBack() - : navigation.navigate("Home", { screen: "StreamList" }); - }} - > - - - - {profile?.handle} - - - - {/* Right Controls Column */} - - {shouldShowFloatingMetrics && ( - - + + { + navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate("Home", { screen: "StreamList" }); + }} + > + + + - - + /> + {profile?.handle} - )} - - - + - {shouldShowFloatingMetrics && isLive && ( + {/* Right Controls Column */} - + {shouldShowFloatingMetrics && ( + + + + + + )} + + + + {shouldShowFloatingMetrics && isLive && ( + + + + )} + + {isSelfAndNotLive && ( + )} - - {isSelfAndNotLive && ( - - )} - { - if (!recordSubmitted && title != "") { - setShowLoading(true); - } - setShowCountdown(false); - }} - /> - + { + if (!recordSubmitted && title != "") { + setShowLoading(true); + } + setShowCountdown(false); + }} + /> + - - + + {showChat === undefined && ( -- 2.51.2 From 76964d79513e9bea4177b43cab08f8dcf0931d66 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 15:28:45 -0500 Subject: [PATCH 019/525] modify size of action col. a bit --- js/app/components/mobile/ui.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 3f04bbeb..b1a67513 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -392,7 +392,6 @@ function RightControlsPanel({ -- 2.51.2 From ba139378b710100cf2626725156ba78d2d44db75 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 15:45:28 -0500 Subject: [PATCH 020/525] readd gesture detector closer --- js/app/components/mobile/ui.tsx | 87 +++++++++++++++++---------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index b1a67513..04305a74 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -288,57 +288,58 @@ export function MobileUi({ /> - {showChat === undefined && ( - - )} - {muted && ( - - { - if (muteWasForced) { - setMuted(false); - setMuteWasForced(false); - } else { - setMuted(false); - } - }} + {showChat === undefined && ( + + )} + {muted && ( + - { + if (muteWasForced) { + setMuted(false); + setMuteWasForced(false); + } else { + setMuted(false); + } + }} style={[ { - padding: 4, - backgroundColor: "rgba(50, 30, 30, 0.4)", - borderRadius: 999, - borderWidth: 2, - borderColor: "rgba(255, 120, 120, 0.2)", + flexDirection: "row", + alignItems: "center", + gap: 8, }, ]} > - - - - Tap to unmute - - - - )} - - + + + + + Tap to unmute + + + + )} + + + ); } -- 2.51.2 From 7e43de8658c763d3708d6729332f16dc82d97705 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 22 Sep 2025 13:54:59 -0700 Subject: [PATCH 021/525] more rust mad science --- Makefile | 2 ++ pkg/cmd/streamplace.go | 10 +++++++--- .../iroh_streamplace/iroh_streamplace.go | 18 ++++++++++++------ .../iroh_streamplace/iroh_streamplace.h | 2 +- rust/iroh-streamplace/src/c2pa.rs | 8 +++++--- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index a9541511..35505d67 100644 --- a/Makefile +++ b/Makefile @@ -974,3 +974,5 @@ ci-download-file: --header "JOB-TOKEN: $$CI_JOB_TOKEN" \ -o bin/$(download_file) \ "$$CI_API_V4_URL/projects/$$CI_PROJECT_ID/packages/generic/$(BRANCH)/$(VERSION)/$(download_file)"; + +c2pa-types: diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 3dbdfa13..b85bf81d 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -52,10 +52,14 @@ type jobFunc func(ctx context.Context, cli *config.CLI) error // parse the CLI and fire up an streamplace node! func start(build *config.BuildFlags, platformJobs []jobFunc) error { - rustErr := iroh_streamplace.PrintCert("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") - panic(rustErr.Error()) + bs, err := os.ReadFile("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") + if err != nil { + return err + } + cert, _ := iroh_streamplace.PrintCert(bs) + panic(cert) selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" - err := media.RunSelfTest(context.Background()) + err = media.RunSelfTest(context.Background()) if err != nil { if selfTest { fmt.Println(err.Error()) diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index 751c8e6a..75b63471 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -353,7 +353,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_func_print_cert() }) - if checksum != 62027 { + if checksum != 39427 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_print_cert: UniFFI API checksum mismatch") } @@ -2111,10 +2111,16 @@ func iroh_streamplace_uniffiFreeGorutine(data C.uint64_t) { guard <- struct{}{} } -func PrintCert(path string) *CertError { - _, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) bool { - C.uniffi_iroh_streamplace_fn_func_print_cert(FfiConverterStringINSTANCE.Lower(path), _uniffiStatus) - return false +func PrintCert(data []byte) (string, *CertError) { + _uniffiRV, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_func_print_cert(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + } }) - return _uniffiErr + if _uniffiErr != nil { + var _uniffiDefaultValue string + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterStringINSTANCE.Lift(_uniffiRV), _uniffiErr + } } diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 3f72811d..02ca4d6c 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -573,7 +573,7 @@ uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT -void uniffi_iroh_streamplace_fn_func_print_cert(RustBuffer path, RustCallStatus *out_status +RustBuffer uniffi_iroh_streamplace_fn_func_print_cert(RustBuffer data, RustCallStatus *out_status ); #endif #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs index de94aca8..8d505e6e 100644 --- a/rust/iroh-streamplace/src/c2pa.rs +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -1,4 +1,5 @@ use c2pa::Reader; +use std::io::Cursor; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] @@ -10,14 +11,15 @@ pub enum CertError { } #[uniffi::export] -pub fn print_cert(path: &str) -> Result<(), CertError> { - let reader = Reader::from_file(path).map_err(|e| CertError::C2paError(e.to_string()))?; +pub fn print_cert(data: Vec) -> Result { + let reader = Reader::from_stream("video/mp4", Cursor::new(data)) + .map_err(|e| CertError::C2paError(e.to_string()))?; // todo: add cawg certs here?? if let Some(manifest) = reader.active_manifest() { if let Some(si) = manifest.signature_info() { println!("{}", si.cert_chain()); // todo: add ocsp validation info - return Ok(()); + return Ok(si.cert_chain().to_string()); } } Err(CertError::NoCertificateChainFound) -- 2.51.2 From 70be52b3b6b323bf41c88874e81e4abe035c5b50 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 22 Sep 2025 16:01:43 -0700 Subject: [PATCH 022/525] components: force Opus stereo audio when supported --- js/app/package.json | 2 ++ js/components/package.json | 1 + .../components/mobile-player/use-webrtc.tsx | 31 +++++++++++++++++++ pnpm-lock.yaml | 21 +++++++++++++ 4 files changed, 55 insertions(+) diff --git a/js/app/package.json b/js/app/package.json index 99c52a94..b965b025 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -91,6 +91,7 @@ "react-use-websocket": "^4.13.0", "reanimated-color-picker": "^4.0.0", "rtcaudiodevice": "git+https://github.com/streamplace/RTCAudioDevice.git#918e08a0f6f0818fb495a0db0b696b44d11d1336", + "sdp-transform": "^2.15.0", "stream-http": "^3.2.0", "streamplace": "workspace:*", "ua-parser-js": "^2.0.0-rc.1", @@ -111,6 +112,7 @@ "@types/babel__plugin-transform-runtime": "^7", "@types/qrcode": "^1", "@types/react": "~18.3.12", + "@types/sdp-transform": "^2.15.0", "@types/uuid": "^10.0.0", "typescript": "~5.3.3" }, diff --git a/js/components/package.json b/js/components/package.json index 5f2f9d14..7d47d707 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -17,6 +17,7 @@ "author": "Streamplace", "license": "MIT", "devDependencies": { + "@types/sdp-transform": "^2.15.0", "tsup": "^8.5.0" }, "dependencies": { diff --git a/js/components/src/components/mobile-player/use-webrtc.tsx b/js/components/src/components/mobile-player/use-webrtc.tsx index dbe1e2ae..676d79dd 100644 --- a/js/components/src/components/mobile-player/use-webrtc.tsx +++ b/js/components/src/components/mobile-player/use-webrtc.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import * as sdpTransform from "sdp-transform"; import { PlayerStatus, usePlayerStore, useStreamKey } from "../.."; import { RTCPeerConnection, RTCSessionDescription } from "./webrtc-primitives"; @@ -107,6 +108,13 @@ export async function negotiateConnectionWithClientOffer( offerToReceiveAudio: true, offerToReceiveVideo: true, }); + if (!offer.sdp) { + throw Error("no SDP in offer"); + } + + const newSDP = forceStereoAudio(offer.sdp); + + offer.sdp = newSDP; /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */ await peerConnection.setLocalDescription(offer); @@ -297,3 +305,26 @@ export function useWebRTCIngest({ return [mediaStream, setMediaStream]; } + +export function forceStereoAudio(sdp: string): string { + const parsedSDP = sdpTransform.parse(sdp); + const audioMedia = parsedSDP.media.find((m) => m.type === "audio"); + if (!audioMedia) { + throw Error("no audio media in SDP"); + } + const opusCodec = audioMedia.rtp.find((c) => c.codec === "opus"); + if (!opusCodec) { + throw Error("no opus codec in SDP"); + } + const opusFMTP = audioMedia.fmtp.find((c) => c.payload === opusCodec.payload); + if (!opusFMTP) { + throw Error("no opus fmtp in SDP"); + } + const opusParams = sdpTransform.parseParams(opusFMTP.config); + opusParams.stereo = 1; + const newParams = Object.entries(opusParams) + .map(([k, v]) => `${k}=${v}`) + .join(";"); + opusFMTP.config = newParams; + return sdpTransform.write(parsedSDP); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dd139c9..ab76ab3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: rtcaudiodevice: specifier: git+https://github.com/streamplace/RTCAudioDevice.git#918e08a0f6f0818fb495a0db0b696b44d11d1336 version: https://codeload.github.com/streamplace/RTCAudioDevice/tar.gz/918e08a0f6f0818fb495a0db0b696b44d11d1336 + sdp-transform: + specifier: ^2.15.0 + version: 2.15.0 stream-http: specifier: ^3.2.0 version: 3.2.0 @@ -304,6 +307,9 @@ importers: '@types/react': specifier: ~18.3.12 version: 18.3.12 + '@types/sdp-transform': + specifier: ^2.15.0 + version: 2.15.0 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -447,6 +453,9 @@ importers: specifier: ^5.0.5 version: 5.0.5(immer@10.1.1)(react@19.0.0)(use-sync-external-store@1.2.2(react@19.0.0)) devDependencies: + '@types/sdp-transform': + specifier: ^2.15.0 + version: 2.15.0 tsup: specifier: ^8.5.0 version: 8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1) @@ -4622,6 +4631,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/sdp-transform@2.15.0': + resolution: {integrity: sha512-ikIFF0EaYt/2XetIYYVeMj6SB52oVXFasJUXDzWHgzNJS5ep2Pbsu7f8f3Za+dEie8HQtt3Zr9mHYBpWT0XgxQ==} + '@types/send@0.17.4': resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} @@ -10711,6 +10723,10 @@ packages: resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} engines: {node: '>= 12.13.0'} + sdp-transform@2.15.0: + resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==} + hasBin: true + select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} @@ -11565,6 +11581,7 @@ packages: ua-parser-js@2.0.0-rc.1: resolution: {integrity: sha512-H+kTJv7j04XbMuASIIrp7TwKj8rVHfGLR9dI36FAWneGHyQ6JZVwFukFgxSyM/OChmk7dxsV82R/tnEQxb1EXg==} + deprecated: You are using an early pre-release version of ua-parser-js v2.x which may be unstable. Please update to the latest stable ua-parser-js v2.x release (https://uaparser.dev) hasBin: true uc.micro@1.0.6: @@ -18369,6 +18386,8 @@ snapshots: dependencies: '@types/node': 22.15.17 + '@types/sdp-transform@2.15.0': {} + '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 @@ -26372,6 +26391,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.16.0) ajv-keywords: 5.1.0(ajv@8.16.0) + sdp-transform@2.15.0: {} + select-hose@2.0.0: {} selfsigned@2.4.1: -- 2.51.2 From 8ff9868e124ec89c319742b1a26668b07042a3d6 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 22 Sep 2025 19:01:39 -0500 Subject: [PATCH 023/525] make buttons 1:1 ratio --- js/components/src/components/chat/chat-box.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/js/components/src/components/chat/chat-box.tsx b/js/components/src/components/chat/chat-box.tsx index 4ed8a659..f6eaba2a 100644 --- a/js/components/src/components/chat/chat-box.tsx +++ b/js/components/src/components/chat/chat-box.tsx @@ -421,7 +421,8 @@ export function ChatBox({ > {/* Livestream Panel */} diff --git a/js/app/src/screens/chat-popout.native.tsx b/js/app/src/screens/chat-popout.native.tsx index 371e5b13..030aedc4 100644 --- a/js/app/src/screens/chat-popout.native.tsx +++ b/js/app/src/screens/chat-popout.native.tsx @@ -1,5 +1,92 @@ -import { View } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import { + Chat, + ChatBox, + LivestreamProvider, + PlayerProvider, + Text, + useKeyboard, + usePlayerStore, + zero, +} from "@streamplace/components"; +import emojiData from "assets/emoji-data.json"; +import { selectUserProfile } from "features/bluesky/blueskySlice"; +import { ArrowLeft } from "lucide-react-native"; +import { useEffect } from "react"; +import { KeyboardAvoidingView, Pressable, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useAppSelector } from "store/hooks"; export default function PopoutChat({ route }) { - return ; + const user = route.params?.user; + if (typeof user !== "string") { + return ( + + No user specified + + ); + } + + return ( + + + + + + ); +} + +export function PopoutChatInner({ user }: { user: string }) { + const setSrc = usePlayerStore((x) => x.setSrc); + const profile = useAppSelector(selectUserProfile); + const navigation = useNavigation(); + useEffect(() => { + setSrc(user); + }, [user]); + const safe = useSafeAreaInsets(); + const kb = useKeyboard(); + return ( + + + navigation.goBack()} + style={[ + zero.layout.flex.row, + zero.layout.flex.align.center, + zero.p[3], + zero.mb[2], + { + backgroundColor: "rgba(255, 255, 255, 0.1)", + borderRadius: 8, + }, + ]} + > + + Go Back + + + + {profile && } + + + + ); } -- 2.51.2 From 50c3194143f8d7c2d3f35406900d78ffafa63d7e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Sep 2025 14:32:58 -0500 Subject: [PATCH 034/525] add more context around chat only view on mobile --- js/app/src/screens/chat-popout.native.tsx | 112 +++++++++++++++++++--- 1 file changed, 100 insertions(+), 12 deletions(-) diff --git a/js/app/src/screens/chat-popout.native.tsx b/js/app/src/screens/chat-popout.native.tsx index 030aedc4..968356e8 100644 --- a/js/app/src/screens/chat-popout.native.tsx +++ b/js/app/src/screens/chat-popout.native.tsx @@ -6,13 +6,16 @@ import { PlayerProvider, Text, useKeyboard, + useLivestreamInfo, usePlayerStore, + useSegment, zero, } from "@streamplace/components"; import emojiData from "assets/emoji-data.json"; +import LiveDot from "components/home/live-dot"; import { selectUserProfile } from "features/bluesky/blueskySlice"; import { ArrowLeft } from "lucide-react-native"; -import { useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { KeyboardAvoidingView, Pressable, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useAppSelector } from "store/hooks"; @@ -46,11 +49,41 @@ export function PopoutChatInner({ user }: { user: string }) { const setSrc = usePlayerStore((x) => x.setSrc); const profile = useAppSelector(selectUserProfile); const navigation = useNavigation(); + const { ingest, profile: streamProfile } = useLivestreamInfo(); + const status = usePlayerStore((x) => x.status); + const seg = useSegment(); + + const segmentReceivedTimeRef = useRef(null); + const lastSegmentIdRef = useRef(null); + + // Track when we receive a new segment + useEffect(() => { + if (seg && seg.id !== lastSegmentIdRef.current) { + segmentReceivedTimeRef.current = Date.now(); + lastSegmentIdRef.current = seg.id; + } + }, [seg?.id]); + + const getLatency = useCallback((): string => { + if (!segmentReceivedTimeRef.current) return ""; + const secondsAgo = Math.floor(Date.now() - segmentReceivedTimeRef.current); + const isThreeDigits = secondsAgo >= 100 && secondsAgo < 1000; + if (isThreeDigits) { + return `0${secondsAgo}`; + } + return `${secondsAgo}`; + }, [seg?.id]); // depend on seg.id to update when new segments arrive + useEffect(() => { setSrc(user); }, [user]); + const safe = useSafeAreaInsets(); const kb = useKeyboard(); + + // if a seg exists and it started less than 10 seconds ago, consider it live + const isLive = seg && Date.now() - new Date(seg.startTime).getTime() < 10000; + return ( - navigation.goBack()} + - - Go Back - + navigation.goBack()} + style={[ + zero.layout.flex.row, + zero.layout.flex.align.center, + zero.p[3], + { borderRadius: zero.borderRadius.xl }, + ]} + > + + + + + {isLive ? ( + + ) : ( + + )} + + {streamProfile?.handle || user} + + + + + {isLive ? "Live" : "Offline"} + + {isLive && ( + + {getLatency()}ms + + )} + + + {profile && } -- 2.51.2 From b7de202d057f7cd531c8b461a79aa03e24963797 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 23 Sep 2025 15:28:02 -0700 Subject: [PATCH 035/525] c2pa: add working c2patypes class --- pkg/c2pa/Builder.schema.json | 1345 ----------------- pkg/c2pa/ManifestDefinition.schema.json | 1271 ---------------- pkg/c2patypes/c2patypes.go | 925 ++++++++++++ .../json/Everything.schema.json} | 880 +++++------ pkg/cmd/streamplace.go | 15 +- .../iroh_streamplace/iroh_streamplace.go | 23 + .../iroh_streamplace/iroh_streamplace.h | 11 + rust/iroh-streamplace/src/c2pa.rs | 11 + 8 files changed, 1402 insertions(+), 3079 deletions(-) delete mode 100644 pkg/c2pa/Builder.schema.json delete mode 100644 pkg/c2pa/ManifestDefinition.schema.json create mode 100644 pkg/c2patypes/c2patypes.go rename pkg/{c2pa/Reader.schema.json => c2patypes/json/Everything.schema.json} (68%) diff --git a/pkg/c2pa/Builder.schema.json b/pkg/c2pa/Builder.schema.json deleted file mode 100644 index 02f6a282..00000000 --- a/pkg/c2pa/Builder.schema.json +++ /dev/null @@ -1,1345 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Builder", - "description": "Use a Builder to add a signed manifest to an asset.\n\n# Example: Building and signing a manifest\n\n``` use c2pa::Result; use std::path::PathBuf;\n\nuse c2pa::{create_signer, Builder, SigningAlg}; use serde::Serialize; use serde_json::json; use tempfile::tempdir;\n\n#[derive(Serialize)] struct Test { my_tag: usize, }\n\n# fn main() -> Result<()> { #[cfg(feature = \"file_io\")] { let manifest_json = json!({ \"claim_generator_info\": [ { \"name\": \"c2pa_test\", \"version\": \"1.0.0\" } ], \"title\": \"Test_Manifest\" }).to_string();\n\nlet mut builder = Builder::from_json(&manifest_json)?; builder.add_assertion(\"org.contentauth.test\", &Test { my_tag: 42 })?;\n\nlet source = PathBuf::from(\"tests/fixtures/C.jpg\"); let dir = tempdir()?; let dest = dir.path().join(\"test_file.jpg\");\n\n// Create a ps256 signer using certs and key files. TO DO: Update example. let signcert_path = \"tests/fixtures/certs/ps256.pub\"; let pkey_path = \"tests/fixtures/certs/ps256.pem\"; let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;\n\n// embed a manifest using the signer builder.sign_file( signer.as_ref(), &source, &dest)?; } # Ok(()) # } ```", - "type": "object", - "required": [ - "no_embed" - ], - "properties": { - "assertions": { - "description": "A list of assertions", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/AssertionDefinition" - } - }, - "base_path": { - "description": "Base path to search for resources.", - "type": [ - "string", - "null" - ] - }, - "builder_flow": { - "description": "The type of builder being used.", - "anyOf": [ - { - "$ref": "#/definitions/BuilderFlow" - }, - { - "type": "null" - } - ] - }, - "claim_generator_info": { - "description": "Claim Generator Info is always required with at least one entry", - "default": [ - { - "name": "c2pa-rs", - "version": "0.58.0" - } - ], - "type": "array", - "items": { - "$ref": "#/definitions/ClaimGeneratorInfo" - } - }, - "claim_version": { - "description": "The version of the claim. Defaults to 1.", - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "format": { - "description": "The format of the source file as a MIME type.", - "default": "application/octet-stream", - "type": "string" - }, - "ingredients": { - "description": "A List of ingredients", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Ingredient" - } - }, - "instance_id": { - "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "default": "xmp:iid:4caa88cb-6034-4d42-8969-409379d34945", - "type": "string" - }, - "label": { - "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", - "type": [ - "string", - "null" - ] - }, - "metadata": { - "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssertionMetadata" - } - }, - "no_embed": { - "description": "If true, the manifest store will not be embedded in the asset on sign", - "type": "boolean" - }, - "redactions": { - "description": "A list of redactions - URIs to redacted assertions.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "remote_url": { - "description": "Optional remote URL for the manifest", - "type": [ - "string", - "null" - ] - }, - "thumbnail": { - "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "title": { - "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] - }, - "vendor": { - "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", - "type": [ - "string", - "null" - ] - } - }, - "definitions": { - "Actor": { - "description": "Identifies a person responsible for an action.", - "type": "object", - "properties": { - "credentials": { - "description": "List of references to W3C Verifiable Credentials.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/HashedUri" - } - }, - "identifier": { - "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", - "type": [ - "string", - "null" - ] - } - } - }, - "AssertionData": { - "description": "This allows the assertion to be expressed as CBOR or JSON. The default is CBOR unless you specify that an assertion should be JSON.", - "anyOf": [ - true - ] - }, - "AssertionDefinition": { - "description": "Defines an assertion that consists of a label that can be either a C2PA-defined assertion label or a custom label in reverse domain format.", - "type": "object", - "required": [ - "data", - "label" - ], - "properties": { - "data": { - "$ref": "#/definitions/AssertionData" - }, - "label": { - "type": "string" - } - } - }, - "AssertionMetadata": { - "description": "The AssertionMetadata structure can be used as part of other assertions or on its own to reference others", - "type": "object", - "properties": { - "dataSource": { - "anyOf": [ - { - "$ref": "#/definitions/DataSource" - }, - { - "type": "null" - } - ] - }, - "dateTime": { - "anyOf": [ - { - "$ref": "#/definitions/DateT" - }, - { - "type": "null" - } - ] - }, - "reference": { - "anyOf": [ - { - "$ref": "#/definitions/HashedUri" - }, - { - "type": "null" - } - ] - }, - "regionOfInterest": { - "anyOf": [ - { - "$ref": "#/definitions/RegionOfInterest" - }, - { - "type": "null" - } - ] - }, - "reviewRatings": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/ReviewRating" - } - } - } - }, - "AssetType": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string" - }, - "version": { - "type": [ - "string", - "null" - ] - } - } - }, - "BuilderFlow": { - "description": "Represents the type of builder flow being used.\n\nThis determines how the builder will be used, such as creating a new asset, opening an existing asset, or updating an existing asset.", - "oneOf": [ - { - "type": "string", - "enum": [ - "Open", - "Update" - ] - }, - { - "type": "object", - "required": [ - "Create" - ], - "properties": { - "Create": { - "$ref": "#/definitions/DigitalSourceType" - } - }, - "additionalProperties": false - } - ] - }, - "ClaimGeneratorInfo": { - "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", - "type": "object", - "required": [ - "name" - ], - "properties": { - "icon": { - "description": "hashed URI to the icon (either embedded or remote)", - "anyOf": [ - { - "$ref": "#/definitions/UriOrResource" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "A human readable string naming the claim_generator", - "type": "string" - }, - "operating_system": { - "description": "A human readable string of the OS the claim generator is running on", - "type": [ - "string", - "null" - ] - }, - "version": { - "description": "A human readable string of the product's version", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": true - }, - "Coordinate": { - "description": "An x, y coordinate used for specifying vertices in polygons.", - "type": "object", - "required": [ - "x", - "y" - ], - "properties": { - "x": { - "description": "The coordinate along the x-axis.", - "type": "number", - "format": "double" - }, - "y": { - "description": "The coordinate along the y-axis.", - "type": "number", - "format": "double" - } - } - }, - "DataSource": { - "description": "A description of the source for assertion data", - "type": "object", - "required": [ - "type" - ], - "properties": { - "actors": { - "description": "A list of [`Actor`]s associated with this source.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Actor" - } - }, - "details": { - "description": "A human-readable string giving details about the source of the assertion data.", - "type": [ - "string", - "null" - ] - }, - "type": { - "description": "A value from among the enumerated list indicating the source of the assertion.", - "type": "string" - } - } - }, - "DateT": { - "type": "string" - }, - "DigitalSourceType": { - "oneOf": [ - { - "description": "Media whose digital content is effectively empty, such as a blank canvas or zero-length video.", - "type": "string", - "enum": [ - "Empty" - ] - }, - { - "description": "Data that is the result of algorithmically using a model derived from sampled content and data. Differs from trainedAlgorithmicMedia in that the result isn’t a media type (e.g., image or video) but is a data format (e.g., CSV, pickle).", - "type": "string", - "enum": [ - "TrainedAlgorithmicData" - ] - } - ] - }, - "Frame": { - "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", - "type": "object", - "properties": { - "end": { - "description": "The end of the frame inclusive or the end of the asset if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "start": { - "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "HashedUri": { - "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", - "type": "object", - "required": [ - "hash", - "url" - ], - "properties": { - "alg": { - "description": "A string identifying the cryptographic hash algorithm used to compute the hash", - "type": [ - "string", - "null" - ] - }, - "hash": { - "description": "Byte string containing the hash value", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "url": { - "description": "JUMBF URI reference", - "type": "string" - } - } - }, - "Ingredient": { - "description": "An `Ingredient` is any external asset that has been used in the creation of an asset.", - "type": "object", - "properties": { - "active_manifest": { - "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", - "type": [ - "string", - "null" - ] - }, - "data": { - "description": "A reference to the actual data of the ingredient.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "data_types": { - "description": "Additional information about the data's type to the ingredient V2 structure.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetType" - } - }, - "description": { - "description": "Additional description of the ingredient.", - "type": [ - "string", - "null" - ] - }, - "document_id": { - "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "format": { - "description": "The format of the source file as a MIME type.", - "type": [ - "string", - "null" - ] - }, - "hash": { - "description": "An optional hash of the asset to prevent duplicates.", - "type": [ - "string", - "null" - ] - }, - "informational_URI": { - "description": "URI to an informational page about the ingredient or its data.", - "type": [ - "string", - "null" - ] - }, - "instance_id": { - "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "label": { - "description": "The ingredient's label as assigned in the manifest.", - "type": [ - "string", - "null" - ] - }, - "manifest_data": { - "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "description": "Any additional [`Metadata`] as defined in the C2PA spec.\n\n[`Metadata`]: crate::Metadata", - "anyOf": [ - { - "$ref": "#/definitions/AssertionMetadata" - }, - { - "type": "null" - } - ] - }, - "provenance": { - "description": "URI from `dcterms:provenance` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "relationship": { - "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", - "default": "componentOf", - "allOf": [ - { - "$ref": "#/definitions/Relationship" - } - ] - }, - "resources": { - "readOnly": true, - "allOf": [ - { - "$ref": "#/definitions/ResourceStore" - } - ] - }, - "thumbnail": { - "description": "A thumbnail image capturing the visual state at the time of import.\n\nA tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "title": { - "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] - }, - "validation_results": { - "description": "Validation results (Ingredient.V3)", - "anyOf": [ - { - "$ref": "#/definitions/ValidationResults" - }, - { - "type": "null" - } - ] - }, - "validation_status": { - "description": "Validation status (Ingredient v1 & v2)", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/ValidationStatus" - } - } - } - }, - "IngredientDeltaValidationResult": { - "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", - "type": "object", - "required": [ - "ingredientAssertionURI", - "validationDeltas" - ], - "properties": { - "ingredientAssertionURI": { - "description": "JUMBF URI reference to the ingredient assertion", - "type": "string" - }, - "validationDeltas": { - "description": "Validation results for the ingredient's active manifest", - "allOf": [ - { - "$ref": "#/definitions/StatusCodes" - } - ] - } - } - }, - "Item": { - "description": "Description of the boundaries of an identified range.", - "type": "object", - "required": [ - "identifier", - "value" - ], - "properties": { - "identifier": { - "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", - "type": "string" - }, - "value": { - "description": "The value of the identifier, e.g. a value of \"2\" for an identifier of \"track_id\" would imply track 2 of the asset.", - "type": "string" - } - } - }, - "Range": { - "description": "A spatial, temporal, frame, or textual range describing the region of interest.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "frame": { - "description": "A frame range.", - "anyOf": [ - { - "$ref": "#/definitions/Frame" - }, - { - "type": "null" - } - ] - }, - "item": { - "description": "A item identifier.", - "anyOf": [ - { - "$ref": "#/definitions/Item" - }, - { - "type": "null" - } - ] - }, - "shape": { - "description": "A spatial range.", - "anyOf": [ - { - "$ref": "#/definitions/Shape" - }, - { - "type": "null" - } - ] - }, - "text": { - "description": "A textual range.", - "anyOf": [ - { - "$ref": "#/definitions/Text" - }, - { - "type": "null" - } - ] - }, - "time": { - "description": "A temporal range.", - "anyOf": [ - { - "$ref": "#/definitions/Time" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": "The type of range of interest.", - "allOf": [ - { - "$ref": "#/definitions/RangeType" - } - ] - } - } - }, - "RangeType": { - "description": "The type of range for the region of interest.", - "oneOf": [ - { - "description": "A spatial range, see [`Shape`] for more details.", - "type": "string", - "enum": [ - "spatial" - ] - }, - { - "description": "A temporal range, see [`Time`] for more details.", - "type": "string", - "enum": [ - "temporal" - ] - }, - { - "description": "A spatial range, see [`Frame`] for more details.", - "type": "string", - "enum": [ - "frame" - ] - }, - { - "description": "A textual range, see [`Text`] for more details.", - "type": "string", - "enum": [ - "textual" - ] - }, - { - "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", - "type": "string", - "enum": [ - "identified" - ] - } - ] - }, - "RegionOfInterest": { - "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", - "type": "object", - "required": [ - "region" - ], - "properties": { - "description": { - "description": "A free-text string.", - "type": [ - "string", - "null" - ] - }, - "identifier": { - "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", - "type": [ - "string", - "null" - ] - }, - "metadata": { - "description": "Additional information about the asset.", - "anyOf": [ - { - "$ref": "#/definitions/AssertionMetadata" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", - "type": [ - "string", - "null" - ] - }, - "region": { - "description": "A range describing the region of interest for the specific asset.", - "type": "array", - "items": { - "$ref": "#/definitions/Range" - } - }, - "role": { - "description": "A value from our controlled vocabulary or an entity-specific value (e.g., com.litware.coolArea) that represents the role of a region among other regions.", - "anyOf": [ - { - "$ref": "#/definitions/Role" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", - "type": [ - "string", - "null" - ] - } - } - }, - "Relationship": { - "description": "The relationship of the ingredient to the current asset.", - "oneOf": [ - { - "description": "The current asset is derived from this ingredient.", - "type": "string", - "enum": [ - "parentOf" - ] - }, - { - "description": "The current asset is a part of this ingredient.", - "type": "string", - "enum": [ - "componentOf" - ] - }, - { - "description": "The ingredient was used as an input to a computational process to create or modify the asset.", - "type": "string", - "enum": [ - "inputTo" - ] - } - ] - }, - "ResourceRef": { - "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", - "type": "object", - "required": [ - "format", - "identifier" - ], - "properties": { - "alg": { - "description": "The algorithm used to hash the resource (if applicable).", - "type": [ - "string", - "null" - ] - }, - "data_types": { - "description": "More detailed data types as defined in the C2PA spec.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetType" - } - }, - "format": { - "description": "The mime type of the referenced resource.", - "type": "string" - }, - "hash": { - "description": "The hash of the resource (if applicable).", - "type": [ - "string", - "null" - ] - }, - "identifier": { - "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", - "type": "string" - } - } - }, - "ResourceStore": { - "description": "Resource store to contain binary objects referenced from JSON serializable structures", - "type": "object", - "required": [ - "resources" - ], - "properties": { - "base_path": { - "type": [ - "string", - "null" - ] - }, - "label": { - "type": [ - "string", - "null" - ] - }, - "resources": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - } - } - }, - "ReviewRating": { - "description": "A rating on an Assertion.\n\nSee .", - "type": "object", - "required": [ - "explanation", - "value" - ], - "properties": { - "code": { - "type": [ - "string", - "null" - ] - }, - "explanation": { - "type": "string" - }, - "value": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - }, - "Role": { - "description": "A role describing the region.", - "oneOf": [ - { - "description": "Arbitrary area worth identifying.", - "type": "string", - "enum": [ - "c2pa.areaOfInterest" - ] - }, - { - "description": "This area is all that is left after a crop action.", - "type": "string", - "enum": [ - "c2pa.cropped" - ] - }, - { - "description": "This area has had edits applied to it.", - "type": "string", - "enum": [ - "c2pa.edited" - ] - }, - { - "description": "The area where an ingredient was placed/added.", - "type": "string", - "enum": [ - "c2pa.placed" - ] - }, - { - "description": "Something in this area was redacted.", - "type": "string", - "enum": [ - "c2pa.redacted" - ] - }, - { - "description": "Area specific to a subject (human or not).", - "type": "string", - "enum": [ - "c2pa.subjectArea" - ] - }, - { - "description": "A range of information was removed/deleted.", - "type": "string", - "enum": [ - "c2pa.deleted" - ] - }, - { - "description": "Styling was applied to this area.", - "type": "string", - "enum": [ - "c2pa.styled" - ] - }, - { - "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", - "type": "string", - "enum": [ - "c2pa.watermarked" - ] - } - ] - }, - "Shape": { - "description": "A spatial range representing rectangle, circle, or a polygon.", - "type": "object", - "required": [ - "origin", - "type", - "unit" - ], - "properties": { - "height": { - "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "inside": { - "description": "If the range is inside the shape.\n\nThe default value is true.", - "type": [ - "boolean", - "null" - ] - }, - "origin": { - "description": "THe origin of the coordinate in the shape.", - "allOf": [ - { - "$ref": "#/definitions/Coordinate" - } - ] - }, - "type": { - "description": "The type of shape.", - "allOf": [ - { - "$ref": "#/definitions/ShapeType" - } - ] - }, - "unit": { - "description": "The type of unit for the shape range.", - "allOf": [ - { - "$ref": "#/definitions/UnitType" - } - ] - }, - "vertices": { - "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Coordinate" - } - }, - "width": { - "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", - "type": [ - "number", - "null" - ], - "format": "double" - } - } - }, - "ShapeType": { - "description": "The type of shape for the range.", - "oneOf": [ - { - "description": "A rectangle.", - "type": "string", - "enum": [ - "rectangle" - ] - }, - { - "description": "A circle.", - "type": "string", - "enum": [ - "circle" - ] - }, - { - "description": "A polygon.", - "type": "string", - "enum": [ - "polygon" - ] - } - ] - }, - "StatusCodes": { - "description": "Contains a set of success, informational, and failure validation status codes.", - "type": "object", - "required": [ - "failure", - "informational", - "success" - ], - "properties": { - "failure": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - }, - "informational": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - }, - "success": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - } - } - }, - "Text": { - "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", - "type": "object", - "required": [ - "selectors" - ], - "properties": { - "selectors": { - "description": "The ranges of text to select.", - "type": "array", - "items": { - "$ref": "#/definitions/TextSelectorRange" - } - } - } - }, - "TextSelector": { - "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", - "type": "object", - "required": [ - "fragment" - ], - "properties": { - "end": { - "description": "The end character offset or the end of the fragment if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "fragment": { - "description": "Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O.", - "type": "string" - }, - "start": { - "description": "The start character offset or the start of the fragment if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "TextSelectorRange": { - "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", - "type": "object", - "required": [ - "selector" - ], - "properties": { - "end": { - "description": "The end of the text range.", - "anyOf": [ - { - "$ref": "#/definitions/TextSelector" - }, - { - "type": "null" - } - ] - }, - "selector": { - "description": "The start (or entire) text range.", - "allOf": [ - { - "$ref": "#/definitions/TextSelector" - } - ] - } - } - }, - "Time": { - "description": "A temporal range representing a starting time to an ending time.", - "type": "object", - "properties": { - "end": { - "description": "The end time or the end of the asset if not present.", - "type": [ - "string", - "null" - ] - }, - "start": { - "description": "The start time or the start of the asset if not present.", - "type": [ - "string", - "null" - ] - }, - "type": { - "description": "The type of time.", - "default": "npt", - "allOf": [ - { - "$ref": "#/definitions/TimeType" - } - ] - } - } - }, - "TimeType": { - "description": "The type of time.", - "oneOf": [ - { - "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", - "type": "string", - "enum": [ - "npt" - ] - } - ] - }, - "UnitType": { - "description": "The type of unit for the range.", - "oneOf": [ - { - "description": "Use pixels.", - "type": "string", - "enum": [ - "pixel" - ] - }, - { - "description": "Use percentage.", - "type": "string", - "enum": [ - "percent" - ] - } - ] - }, - "UriOrResource": { - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "$ref": "#/definitions/HashedUri" - } - ] - }, - "ValidationResults": { - "description": "A map of validation results for a manifest store.\n\nThe map contains the validation results for the active manifest and any ingredient deltas. It is normal for there to be many", - "type": "object", - "properties": { - "activeManifest": { - "anyOf": [ - { - "$ref": "#/definitions/StatusCodes" - }, - { - "type": "null" - } - ] - }, - "ingredientDeltas": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/IngredientDeltaValidationResult" - } - } - } - }, - "ValidationStatus": { - "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", - "type": "object", - "required": [ - "code" - ], - "properties": { - "code": { - "type": "string" - }, - "explanation": { - "type": [ - "string", - "null" - ] - }, - "success": { - "writeOnly": true, - "type": [ - "boolean", - "null" - ] - }, - "url": { - "type": [ - "string", - "null" - ] - } - } - } - } -} \ No newline at end of file diff --git a/pkg/c2pa/ManifestDefinition.schema.json b/pkg/c2pa/ManifestDefinition.schema.json deleted file mode 100644 index 89dcdc9a..00000000 --- a/pkg/c2pa/ManifestDefinition.schema.json +++ /dev/null @@ -1,1271 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ManifestDefinition", - "description": "Use a ManifestDefinition to define a manifest and to build a `ManifestStore`. A manifest is a collection of ingredients and assertions used to define a claim that can be signed and embedded into a file.", - "type": "object", - "properties": { - "assertions": { - "description": "A list of assertions", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/AssertionDefinition" - } - }, - "claim_generator_info": { - "description": "Claim Generator Info is always required with at least one entry", - "default": [ - { - "name": "c2pa-rs", - "version": "0.58.0" - } - ], - "type": "array", - "items": { - "$ref": "#/definitions/ClaimGeneratorInfo" - } - }, - "claim_version": { - "description": "The version of the claim. Defaults to 1.", - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "format": { - "description": "The format of the source file as a MIME type.", - "default": "application/octet-stream", - "type": "string" - }, - "ingredients": { - "description": "A List of ingredients", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Ingredient" - } - }, - "instance_id": { - "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "default": "xmp:iid:36dc43ae-f0d2-4b76-a04f-29ee2f27d67a", - "type": "string" - }, - "label": { - "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", - "type": [ - "string", - "null" - ] - }, - "metadata": { - "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssertionMetadata" - } - }, - "redactions": { - "description": "A list of redactions - URIs to redacted assertions.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "thumbnail": { - "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "title": { - "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] - }, - "vendor": { - "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", - "type": [ - "string", - "null" - ] - } - }, - "definitions": { - "Actor": { - "description": "Identifies a person responsible for an action.", - "type": "object", - "properties": { - "credentials": { - "description": "List of references to W3C Verifiable Credentials.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/HashedUri" - } - }, - "identifier": { - "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", - "type": [ - "string", - "null" - ] - } - } - }, - "AssertionData": { - "description": "This allows the assertion to be expressed as CBOR or JSON. The default is CBOR unless you specify that an assertion should be JSON.", - "anyOf": [ - true - ] - }, - "AssertionDefinition": { - "description": "Defines an assertion that consists of a label that can be either a C2PA-defined assertion label or a custom label in reverse domain format.", - "type": "object", - "required": [ - "data", - "label" - ], - "properties": { - "data": { - "$ref": "#/definitions/AssertionData" - }, - "label": { - "type": "string" - } - } - }, - "AssertionMetadata": { - "description": "The AssertionMetadata structure can be used as part of other assertions or on its own to reference others", - "type": "object", - "properties": { - "dataSource": { - "anyOf": [ - { - "$ref": "#/definitions/DataSource" - }, - { - "type": "null" - } - ] - }, - "dateTime": { - "anyOf": [ - { - "$ref": "#/definitions/DateT" - }, - { - "type": "null" - } - ] - }, - "reference": { - "anyOf": [ - { - "$ref": "#/definitions/HashedUri" - }, - { - "type": "null" - } - ] - }, - "regionOfInterest": { - "anyOf": [ - { - "$ref": "#/definitions/RegionOfInterest" - }, - { - "type": "null" - } - ] - }, - "reviewRatings": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/ReviewRating" - } - } - } - }, - "AssetType": { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string" - }, - "version": { - "type": [ - "string", - "null" - ] - } - } - }, - "ClaimGeneratorInfo": { - "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", - "type": "object", - "required": [ - "name" - ], - "properties": { - "icon": { - "description": "hashed URI to the icon (either embedded or remote)", - "anyOf": [ - { - "$ref": "#/definitions/UriOrResource" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "A human readable string naming the claim_generator", - "type": "string" - }, - "operating_system": { - "description": "A human readable string of the OS the claim generator is running on", - "type": [ - "string", - "null" - ] - }, - "version": { - "description": "A human readable string of the product's version", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": true - }, - "Coordinate": { - "description": "An x, y coordinate used for specifying vertices in polygons.", - "type": "object", - "required": [ - "x", - "y" - ], - "properties": { - "x": { - "description": "The coordinate along the x-axis.", - "type": "number", - "format": "double" - }, - "y": { - "description": "The coordinate along the y-axis.", - "type": "number", - "format": "double" - } - } - }, - "DataSource": { - "description": "A description of the source for assertion data", - "type": "object", - "required": [ - "type" - ], - "properties": { - "actors": { - "description": "A list of [`Actor`]s associated with this source.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Actor" - } - }, - "details": { - "description": "A human-readable string giving details about the source of the assertion data.", - "type": [ - "string", - "null" - ] - }, - "type": { - "description": "A value from among the enumerated list indicating the source of the assertion.", - "type": "string" - } - } - }, - "DateT": { - "type": "string" - }, - "Frame": { - "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", - "type": "object", - "properties": { - "end": { - "description": "The end of the frame inclusive or the end of the asset if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "start": { - "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "HashedUri": { - "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", - "type": "object", - "required": [ - "hash", - "url" - ], - "properties": { - "alg": { - "description": "A string identifying the cryptographic hash algorithm used to compute the hash", - "type": [ - "string", - "null" - ] - }, - "hash": { - "description": "Byte string containing the hash value", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - }, - "url": { - "description": "JUMBF URI reference", - "type": "string" - } - } - }, - "Ingredient": { - "description": "An `Ingredient` is any external asset that has been used in the creation of an asset.", - "type": "object", - "properties": { - "active_manifest": { - "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", - "type": [ - "string", - "null" - ] - }, - "data": { - "description": "A reference to the actual data of the ingredient.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "data_types": { - "description": "Additional information about the data's type to the ingredient V2 structure.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetType" - } - }, - "description": { - "description": "Additional description of the ingredient.", - "type": [ - "string", - "null" - ] - }, - "document_id": { - "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "format": { - "description": "The format of the source file as a MIME type.", - "type": [ - "string", - "null" - ] - }, - "hash": { - "description": "An optional hash of the asset to prevent duplicates.", - "type": [ - "string", - "null" - ] - }, - "informational_URI": { - "description": "URI to an informational page about the ingredient or its data.", - "type": [ - "string", - "null" - ] - }, - "instance_id": { - "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "label": { - "description": "The ingredient's label as assigned in the manifest.", - "type": [ - "string", - "null" - ] - }, - "manifest_data": { - "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "description": "Any additional [`Metadata`] as defined in the C2PA spec.\n\n[`Metadata`]: crate::Metadata", - "anyOf": [ - { - "$ref": "#/definitions/AssertionMetadata" - }, - { - "type": "null" - } - ] - }, - "provenance": { - "description": "URI from `dcterms:provenance` in XMP metadata.", - "type": [ - "string", - "null" - ] - }, - "relationship": { - "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", - "default": "componentOf", - "allOf": [ - { - "$ref": "#/definitions/Relationship" - } - ] - }, - "resources": { - "readOnly": true, - "allOf": [ - { - "$ref": "#/definitions/ResourceStore" - } - ] - }, - "thumbnail": { - "description": "A thumbnail image capturing the visual state at the time of import.\n\nA tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.", - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "type": "null" - } - ] - }, - "title": { - "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] - }, - "validation_results": { - "description": "Validation results (Ingredient.V3)", - "anyOf": [ - { - "$ref": "#/definitions/ValidationResults" - }, - { - "type": "null" - } - ] - }, - "validation_status": { - "description": "Validation status (Ingredient v1 & v2)", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/ValidationStatus" - } - } - } - }, - "IngredientDeltaValidationResult": { - "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", - "type": "object", - "required": [ - "ingredientAssertionURI", - "validationDeltas" - ], - "properties": { - "ingredientAssertionURI": { - "description": "JUMBF URI reference to the ingredient assertion", - "type": "string" - }, - "validationDeltas": { - "description": "Validation results for the ingredient's active manifest", - "allOf": [ - { - "$ref": "#/definitions/StatusCodes" - } - ] - } - } - }, - "Item": { - "description": "Description of the boundaries of an identified range.", - "type": "object", - "required": [ - "identifier", - "value" - ], - "properties": { - "identifier": { - "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", - "type": "string" - }, - "value": { - "description": "The value of the identifier, e.g. a value of \"2\" for an identifier of \"track_id\" would imply track 2 of the asset.", - "type": "string" - } - } - }, - "Range": { - "description": "A spatial, temporal, frame, or textual range describing the region of interest.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "frame": { - "description": "A frame range.", - "anyOf": [ - { - "$ref": "#/definitions/Frame" - }, - { - "type": "null" - } - ] - }, - "item": { - "description": "A item identifier.", - "anyOf": [ - { - "$ref": "#/definitions/Item" - }, - { - "type": "null" - } - ] - }, - "shape": { - "description": "A spatial range.", - "anyOf": [ - { - "$ref": "#/definitions/Shape" - }, - { - "type": "null" - } - ] - }, - "text": { - "description": "A textual range.", - "anyOf": [ - { - "$ref": "#/definitions/Text" - }, - { - "type": "null" - } - ] - }, - "time": { - "description": "A temporal range.", - "anyOf": [ - { - "$ref": "#/definitions/Time" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": "The type of range of interest.", - "allOf": [ - { - "$ref": "#/definitions/RangeType" - } - ] - } - } - }, - "RangeType": { - "description": "The type of range for the region of interest.", - "oneOf": [ - { - "description": "A spatial range, see [`Shape`] for more details.", - "type": "string", - "enum": [ - "spatial" - ] - }, - { - "description": "A temporal range, see [`Time`] for more details.", - "type": "string", - "enum": [ - "temporal" - ] - }, - { - "description": "A spatial range, see [`Frame`] for more details.", - "type": "string", - "enum": [ - "frame" - ] - }, - { - "description": "A textual range, see [`Text`] for more details.", - "type": "string", - "enum": [ - "textual" - ] - }, - { - "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", - "type": "string", - "enum": [ - "identified" - ] - } - ] - }, - "RegionOfInterest": { - "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", - "type": "object", - "required": [ - "region" - ], - "properties": { - "description": { - "description": "A free-text string.", - "type": [ - "string", - "null" - ] - }, - "identifier": { - "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", - "type": [ - "string", - "null" - ] - }, - "metadata": { - "description": "Additional information about the asset.", - "anyOf": [ - { - "$ref": "#/definitions/AssertionMetadata" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", - "type": [ - "string", - "null" - ] - }, - "region": { - "description": "A range describing the region of interest for the specific asset.", - "type": "array", - "items": { - "$ref": "#/definitions/Range" - } - }, - "role": { - "description": "A value from our controlled vocabulary or an entity-specific value (e.g., com.litware.coolArea) that represents the role of a region among other regions.", - "anyOf": [ - { - "$ref": "#/definitions/Role" - }, - { - "type": "null" - } - ] - }, - "type": { - "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", - "type": [ - "string", - "null" - ] - } - } - }, - "Relationship": { - "description": "The relationship of the ingredient to the current asset.", - "oneOf": [ - { - "description": "The current asset is derived from this ingredient.", - "type": "string", - "enum": [ - "parentOf" - ] - }, - { - "description": "The current asset is a part of this ingredient.", - "type": "string", - "enum": [ - "componentOf" - ] - }, - { - "description": "The ingredient was used as an input to a computational process to create or modify the asset.", - "type": "string", - "enum": [ - "inputTo" - ] - } - ] - }, - "ResourceRef": { - "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", - "type": "object", - "required": [ - "format", - "identifier" - ], - "properties": { - "alg": { - "description": "The algorithm used to hash the resource (if applicable).", - "type": [ - "string", - "null" - ] - }, - "data_types": { - "description": "More detailed data types as defined in the C2PA spec.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetType" - } - }, - "format": { - "description": "The mime type of the referenced resource.", - "type": "string" - }, - "hash": { - "description": "The hash of the resource (if applicable).", - "type": [ - "string", - "null" - ] - }, - "identifier": { - "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", - "type": "string" - } - } - }, - "ResourceStore": { - "description": "Resource store to contain binary objects referenced from JSON serializable structures", - "type": "object", - "required": [ - "resources" - ], - "properties": { - "base_path": { - "type": [ - "string", - "null" - ] - }, - "label": { - "type": [ - "string", - "null" - ] - }, - "resources": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - } - } - }, - "ReviewRating": { - "description": "A rating on an Assertion.\n\nSee .", - "type": "object", - "required": [ - "explanation", - "value" - ], - "properties": { - "code": { - "type": [ - "string", - "null" - ] - }, - "explanation": { - "type": "string" - }, - "value": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - } - } - }, - "Role": { - "description": "A role describing the region.", - "oneOf": [ - { - "description": "Arbitrary area worth identifying.", - "type": "string", - "enum": [ - "c2pa.areaOfInterest" - ] - }, - { - "description": "This area is all that is left after a crop action.", - "type": "string", - "enum": [ - "c2pa.cropped" - ] - }, - { - "description": "This area has had edits applied to it.", - "type": "string", - "enum": [ - "c2pa.edited" - ] - }, - { - "description": "The area where an ingredient was placed/added.", - "type": "string", - "enum": [ - "c2pa.placed" - ] - }, - { - "description": "Something in this area was redacted.", - "type": "string", - "enum": [ - "c2pa.redacted" - ] - }, - { - "description": "Area specific to a subject (human or not).", - "type": "string", - "enum": [ - "c2pa.subjectArea" - ] - }, - { - "description": "A range of information was removed/deleted.", - "type": "string", - "enum": [ - "c2pa.deleted" - ] - }, - { - "description": "Styling was applied to this area.", - "type": "string", - "enum": [ - "c2pa.styled" - ] - }, - { - "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", - "type": "string", - "enum": [ - "c2pa.watermarked" - ] - } - ] - }, - "Shape": { - "description": "A spatial range representing rectangle, circle, or a polygon.", - "type": "object", - "required": [ - "origin", - "type", - "unit" - ], - "properties": { - "height": { - "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "inside": { - "description": "If the range is inside the shape.\n\nThe default value is true.", - "type": [ - "boolean", - "null" - ] - }, - "origin": { - "description": "THe origin of the coordinate in the shape.", - "allOf": [ - { - "$ref": "#/definitions/Coordinate" - } - ] - }, - "type": { - "description": "The type of shape.", - "allOf": [ - { - "$ref": "#/definitions/ShapeType" - } - ] - }, - "unit": { - "description": "The type of unit for the shape range.", - "allOf": [ - { - "$ref": "#/definitions/UnitType" - } - ] - }, - "vertices": { - "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Coordinate" - } - }, - "width": { - "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", - "type": [ - "number", - "null" - ], - "format": "double" - } - } - }, - "ShapeType": { - "description": "The type of shape for the range.", - "oneOf": [ - { - "description": "A rectangle.", - "type": "string", - "enum": [ - "rectangle" - ] - }, - { - "description": "A circle.", - "type": "string", - "enum": [ - "circle" - ] - }, - { - "description": "A polygon.", - "type": "string", - "enum": [ - "polygon" - ] - } - ] - }, - "StatusCodes": { - "description": "Contains a set of success, informational, and failure validation status codes.", - "type": "object", - "required": [ - "failure", - "informational", - "success" - ], - "properties": { - "failure": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - }, - "informational": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - }, - "success": { - "type": "array", - "items": { - "$ref": "#/definitions/ValidationStatus" - } - } - } - }, - "Text": { - "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", - "type": "object", - "required": [ - "selectors" - ], - "properties": { - "selectors": { - "description": "The ranges of text to select.", - "type": "array", - "items": { - "$ref": "#/definitions/TextSelectorRange" - } - } - } - }, - "TextSelector": { - "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", - "type": "object", - "required": [ - "fragment" - ], - "properties": { - "end": { - "description": "The end character offset or the end of the fragment if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "fragment": { - "description": "Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O.", - "type": "string" - }, - "start": { - "description": "The start character offset or the start of the fragment if not present.", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "TextSelectorRange": { - "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", - "type": "object", - "required": [ - "selector" - ], - "properties": { - "end": { - "description": "The end of the text range.", - "anyOf": [ - { - "$ref": "#/definitions/TextSelector" - }, - { - "type": "null" - } - ] - }, - "selector": { - "description": "The start (or entire) text range.", - "allOf": [ - { - "$ref": "#/definitions/TextSelector" - } - ] - } - } - }, - "Time": { - "description": "A temporal range representing a starting time to an ending time.", - "type": "object", - "properties": { - "end": { - "description": "The end time or the end of the asset if not present.", - "type": [ - "string", - "null" - ] - }, - "start": { - "description": "The start time or the start of the asset if not present.", - "type": [ - "string", - "null" - ] - }, - "type": { - "description": "The type of time.", - "default": "npt", - "allOf": [ - { - "$ref": "#/definitions/TimeType" - } - ] - } - } - }, - "TimeType": { - "description": "The type of time.", - "oneOf": [ - { - "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", - "type": "string", - "enum": [ - "npt" - ] - } - ] - }, - "UnitType": { - "description": "The type of unit for the range.", - "oneOf": [ - { - "description": "Use pixels.", - "type": "string", - "enum": [ - "pixel" - ] - }, - { - "description": "Use percentage.", - "type": "string", - "enum": [ - "percent" - ] - } - ] - }, - "UriOrResource": { - "anyOf": [ - { - "$ref": "#/definitions/ResourceRef" - }, - { - "$ref": "#/definitions/HashedUri" - } - ] - }, - "ValidationResults": { - "description": "A map of validation results for a manifest store.\n\nThe map contains the validation results for the active manifest and any ingredient deltas. It is normal for there to be many", - "type": "object", - "properties": { - "activeManifest": { - "anyOf": [ - { - "$ref": "#/definitions/StatusCodes" - }, - { - "type": "null" - } - ] - }, - "ingredientDeltas": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/IngredientDeltaValidationResult" - } - } - } - }, - "ValidationStatus": { - "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", - "type": "object", - "required": [ - "code" - ], - "properties": { - "code": { - "type": "string" - }, - "explanation": { - "type": [ - "string", - "null" - ] - }, - "success": { - "writeOnly": true, - "type": [ - "boolean", - "null" - ] - }, - "url": { - "type": [ - "string", - "null" - ] - } - } - } - } -} \ No newline at end of file diff --git a/pkg/c2patypes/c2patypes.go b/pkg/c2patypes/c2patypes.go new file mode 100644 index 00000000..01ec516f --- /dev/null +++ b/pkg/c2patypes/c2patypes.go @@ -0,0 +1,925 @@ +// Code generated from JSON Schema using quicktype. DO NOT EDIT. +// To parse and unparse this JSON data, add this code to your project and do: +// +// c2Patypes, err := UnmarshalC2Patypes(bytes) +// bytes, err = c2Patypes.Marshal() + +package c2patypes + +import "bytes" +import "errors" + +import "encoding/json" + +func UnmarshalC2Patypes(data []byte) (C2Patypes, error) { + var r C2Patypes + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *C2Patypes) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +type C2Patypes struct { + Builder Builder `json:"builder"` + ManifestDefinition ManifestDefinition `json:"manifest_definition"` + Reader Reader `json:"reader"` +} + +// Use a Builder to add a signed manifest to an asset. +// +// # Example: Building and signing a manifest +// +// ``` use c2pa::Result; use std::path::PathBuf; +// +// use c2pa::{create_signer, Builder, SigningAlg}; use serde::Serialize; use +// serde_json::json; use tempfile::tempdir; +// +// #[derive(Serialize)] struct Test { my_tag: usize, } +// +// # fn main() -> Result<()> { #[cfg(feature = "file_io")] { let manifest_json = json!({ +// "claim_generator_info": [ { "name": "c2pa_test", "version": "1.0.0" } ], "title": +// "Test_Manifest" }).to_string(); +// +// let mut builder = Builder::from_json(&manifest_json)?; +// builder.add_assertion("org.contentauth.test", &Test { my_tag: 42 })?; +// +// let source = PathBuf::from("tests/fixtures/C.jpg"); let dir = tempdir()?; let dest = +// dir.path().join("test_file.jpg"); +// +// // Create a ps256 signer using certs and key files. TO DO: Update example. let +// signcert_path = "tests/fixtures/certs/ps256.pub"; let pkey_path = +// "tests/fixtures/certs/ps256.pem"; let signer = create_signer::from_files(signcert_path, +// pkey_path, SigningAlg::Ps256, None)?; +// +// // embed a manifest using the signer builder.sign_file( signer.as_ref(), &source, +// &dest)?; } # Ok(()) # } ``` +type Builder struct { + // A list of assertions + Assertions []AssertionDefinition `json:"assertions,omitempty"` + // Base path to search for resources. + BasePath *string `json:"base_path"` + // The type of builder being used. + BuilderFlow *BuilderFlowUnion `json:"builder_flow"` + // Claim Generator Info is always required with at least one entry + ClaimGeneratorInfo []ClaimGeneratorInfo `json:"claim_generator_info,omitempty"` + // The version of the claim. Defaults to 1. + ClaimVersion *int64 `json:"claim_version"` + // The format of the source file as a MIME type. + Format *string `json:"format,omitempty"` + // A List of ingredients + Ingredients []Ingredient `json:"ingredients,omitempty"` + // Instance ID from `xmpMM:InstanceID` in XMP metadata. + InstanceID *string `json:"instance_id,omitempty"` + // Allows you to pre-define the manifest label, which must be unique. Not intended for + // general use. If not set, it will be assigned automatically. + Label *string `json:"label"` + // Optional manifest metadata. This will be deprecated in the future; not recommended to use. + Metadata []AssertionMetadata `json:"metadata"` + // If true, the manifest store will not be embedded in the asset on sign + NoEmbed bool `json:"no_embed"` + // A list of redactions - URIs to redacted assertions. + Redactions []string `json:"redactions"` + // Optional remote URL for the manifest + RemoteURL *string `json:"remote_url"` + // An optional ResourceRef to a thumbnail image that represents the asset that was signed. + // Must be available when the manifest is signed. + Thumbnail *IngredientResourceRef `json:"thumbnail"` + // A human-readable title, generally source filename. + Title *string `json:"title"` + // Optional prefix added to the generated Manifest Label This is typically a reverse domain + // name. + Vendor *string `json:"vendor"` +} + +// Defines an assertion that consists of a label that can be either a C2PA-defined assertion +// label or a custom label in reverse domain format. +type AssertionDefinition struct { + Data interface{} `json:"data"` + Label string `json:"label"` +} + +type BuilderFlowClass struct { + Create DigitalSourceType `json:"Create"` +} + +// Description of the claim generator, or the software used in generating the claim. +// +// This structure is also used for actions softwareAgent +type ClaimGeneratorInfo struct { + // hashed URI to the icon (either embedded or remote) + Icon *ResourceRef `json:"icon"` + // A human readable string naming the claim_generator + Name string `json:"name"` + // A human readable string of the OS the claim generator is running on + OperatingSystem *string `json:"operating_system"` + // A human readable string of the product's version + Version *string `json:"version"` +} + +// A reference to a resource to be used in JSON serialization. +// +// The underlying data can be read as a stream via +// [`Reader::resource_to_stream`][crate::Reader::resource_to_stream]. +// +// A `HashedUri` provides a reference to content available within the same manifest store. +// +// This is described in [§8.3, URI References], of the C2PA Technical Specification. +// +// [§8.3, URI References]: +// https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references +type ResourceRef struct { + // The algorithm used to hash the resource (if applicable). + // + // A string identifying the cryptographic hash algorithm used to compute the hash + Alg *string `json:"alg"` + // More detailed data types as defined in the C2PA spec. + DataTypes []AssetType `json:"data_types"` + // The mime type of the referenced resource. + Format *string `json:"format,omitempty"` + // The hash of the resource (if applicable). + // + // Byte string containing the hash value + Hash *BasePath `json:"hash"` + // A URI that identifies the resource as referenced from the manifest. + // + // This may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will + // be resolved with the manifest label. Relative file paths will be resolved with the base + // path if provided. + Identifier *string `json:"identifier,omitempty"` + // JUMBF URI reference + URL *string `json:"url,omitempty"` +} + +type AssetType struct { + Type string `json:"type"` + Version *string `json:"version"` +} + +// An `Ingredient` is any external asset that has been used in the creation of an asset. +type Ingredient struct { + // The active manifest label (if one exists). + // + // If this ingredient has a [`ManifestStore`], this will hold the label of the active + // [`Manifest`]. + // + // [`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore + ActiveManifest *string `json:"active_manifest"` + // A reference to the actual data of the ingredient. + Data *IngredientResourceRef `json:"data"` + // Additional information about the data's type to the ingredient V2 structure. + DataTypes []AssetType `json:"data_types"` + // Additional description of the ingredient. + Description *string `json:"description"` + // Document ID from `xmpMM:DocumentID` in XMP metadata. + DocumentID *string `json:"document_id"` + // The format of the source file as a MIME type. + Format *string `json:"format"` + // An optional hash of the asset to prevent duplicates. + Hash *string `json:"hash"` + // URI to an informational page about the ingredient or its data. + InformationalURI *string `json:"informational_URI"` + // Instance ID from `xmpMM:InstanceID` in XMP metadata. + InstanceID *string `json:"instance_id"` + // The ingredient's label as assigned in the manifest. + Label *string `json:"label"` + // A [`ManifestStore`] from the source asset extracted as a binary C2PA blob. + // + // [`ManifestStore`]: crate::ManifestStore + ManifestData *IngredientResourceRef `json:"manifest_data"` + // Any additional [`Metadata`] as defined in the C2PA spec. + // + // [`Metadata`]: crate::Metadata + Metadata *AssertionMetadata `json:"metadata"` + // URI from `dcterms:provenance` in XMP metadata. + Provenance *string `json:"provenance"` + // Set to `ParentOf` if this is the parent ingredient. + // + // There can only be one parent ingredient in the ingredients. + Relationship *Relationship `json:"relationship,omitempty"` + Resources *ResourceStore `json:"resources,omitempty"` + // A thumbnail image capturing the visual state at the time of import. + // + // A tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image. + Thumbnail *IngredientResourceRef `json:"thumbnail"` + // A human-readable title, generally source filename. + Title *string `json:"title"` + // Validation results (Ingredient.V3) + ValidationResults *ValidationResults `json:"validation_results"` + // Validation status (Ingredient v1 & v2) + ValidationStatus []ValidationStatus `json:"validation_status"` +} + +// A reference to a resource to be used in JSON serialization. +// +// The underlying data can be read as a stream via +// [`Reader::resource_to_stream`][crate::Reader::resource_to_stream]. +type IngredientResourceRef struct { + // The algorithm used to hash the resource (if applicable). + Alg *string `json:"alg"` + // More detailed data types as defined in the C2PA spec. + DataTypes []AssetType `json:"data_types"` + // The mime type of the referenced resource. + Format string `json:"format"` + // The hash of the resource (if applicable). + Hash *string `json:"hash"` + // A URI that identifies the resource as referenced from the manifest. + // + // This may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will + // be resolved with the manifest label. Relative file paths will be resolved with the base + // path if provided. + Identifier string `json:"identifier"` +} + +// A region of interest within an asset describing the change. +// +// This struct can be used from [`Action::changes`][crate::assertions::Action::changes] or +// [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest]. +type RegionOfInterest struct { + // A free-text string. + Description *string `json:"description"` + // A free-text string representing a machine-readable, unique to this assertion, identifier + // for the region. + Identifier *string `json:"identifier"` + // Additional information about the asset. + Metadata *AssertionMetadata `json:"metadata"` + // A free-text string representing a human-readable name for the region which might be used + // in a user interface. + Name *string `json:"name"` + // A range describing the region of interest for the specific asset. + Region []Range `json:"region"` + // A value from our controlled vocabulary or an entity-specific value (e.g., + // com.litware.coolArea) that represents the role of a region among other regions. + Role *Role `json:"role"` + // A value from a controlled vocabulary such as + // or an entity-specific value (e.g., + // com.litware.newType) that represents the type of thing(s) depicted by a region. + // + // Note this field serializes/deserializes into the name `type`. + Type *string `json:"type"` +} + +// The AssertionMetadata structure can be used as part of other assertions or on its own to +// reference others +type AssertionMetadata struct { + DataSource *DataSource `json:"dataSource"` + DateTime *string `json:"dateTime"` + Reference *HashedURI `json:"reference"` + RegionOfInterest *RegionOfInterest `json:"regionOfInterest"` + ReviewRatings []ReviewRating `json:"reviewRatings"` +} + +// A spatial, temporal, frame, or textual range describing the region of interest. +type Range struct { + // A frame range. + Frame *Frame `json:"frame"` + // A item identifier. + Item *Item `json:"item"` + // A spatial range. + Shape *Shape `json:"shape"` + // A textual range. + Text *Text `json:"text"` + // A temporal range. + Time *Time `json:"time"` + // The type of range of interest. + Type RangeType `json:"type"` +} + +// A frame range representing starting and ending frames or pages. +// +// If both `start` and `end` are missing, the frame will span the entire asset. +type Frame struct { + // The end of the frame inclusive or the end of the asset if not present. + End *int64 `json:"end"` + // The start of the frame or the end of the asset if not present. + // + // The first frame/page starts at 0. + Start *int64 `json:"start"` +} + +// Description of the boundaries of an identified range. +type Item struct { + // The container-specific term used to identify items, such as "track_id" for MP4 or + // "item_ID" for HEIF. + Identifier string `json:"identifier"` + // The value of the identifier, e.g. a value of "2" for an identifier of "track_id" would + // imply track 2 of the asset. + Value string `json:"value"` +} + +// A spatial range representing rectangle, circle, or a polygon. +type Shape struct { + // The height of a rectnagle. + // + // This field can be ignored for circles and polygons. + Height *float64 `json:"height"` + // If the range is inside the shape. + // + // The default value is true. + Inside *bool `json:"inside"` + // THe origin of the coordinate in the shape. + Origin Coordinate `json:"origin"` + // The type of shape. + Type ShapeType `json:"type"` + // The type of unit for the shape range. + Unit UnitType `json:"unit"` + // The vertices of the polygon. + // + // This field can be ignored for rectangles and circles. + Vertices []Coordinate `json:"vertices"` + // The width for rectangles or diameter for circles. + // + // This field can be ignored for polygons. + Width *float64 `json:"width"` +} + +// THe origin of the coordinate in the shape. +// +// An x, y coordinate used for specifying vertices in polygons. +type Coordinate struct { + // The coordinate along the x-axis. + X float64 `json:"x"` + // The coordinate along the y-axis. + Y float64 `json:"y"` +} + +// A textual range representing multiple (possibly discontinuous) ranges of text. +type Text struct { + // The ranges of text to select. + Selectors []TextSelectorRange `json:"selectors"` +} + +// One or two [`TextSelector`][TextSelector] identifiying the range to select. +type TextSelectorRange struct { + // The end of the text range. + End *TextSelector `json:"end"` + // The start (or entire) text range. + Selector TextSelector `json:"selector"` +} + +// Selects a range of text via a fragment identifier. +// +// This is modeled after the W3C Web Annotation selector model. +// +// The start (or entire) text range. +type TextSelector struct { + // The end character offset or the end of the fragment if not present. + End *int64 `json:"end"` + // Fragment identifier as per RFC3023 (XML) or ISO 32000-2 (PDF), Annex O. + Fragment string `json:"fragment"` + // The start character offset or the start of the fragment if not present. + Start *int64 `json:"start"` +} + +// A temporal range representing a starting time to an ending time. +type Time struct { + // The end time or the end of the asset if not present. + End *string `json:"end"` + // The start time or the start of the asset if not present. + Start *string `json:"start"` + // The type of time. + Type *TimeType `json:"type,omitempty"` +} + +// A description of the source for assertion data +type DataSource struct { + // A list of [`Actor`]s associated with this source. + Actors []Actor `json:"actors"` + // A human-readable string giving details about the source of the assertion data. + Details *string `json:"details"` + // A value from among the enumerated list indicating the source of the assertion. + Type string `json:"type"` +} + +// Identifies a person responsible for an action. +type Actor struct { + // List of references to W3C Verifiable Credentials. + Credentials []HashedURI `json:"credentials"` + // An identifier for a human actor, used when the "type" is `humanEntry.identified`. + Identifier *string `json:"identifier"` +} + +// A `HashedUri` provides a reference to content available within the same manifest store. +// +// This is described in [§8.3, URI References], of the C2PA Technical Specification. +// +// [§8.3, URI References]: +// https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references +type HashedURI struct { + // A string identifying the cryptographic hash algorithm used to compute the hash + Alg *string `json:"alg"` + // Byte string containing the hash value + Hash []int64 `json:"hash"` + // JUMBF URI reference + URL string `json:"url"` +} + +// A rating on an Assertion. +// +// See +// . +type ReviewRating struct { + Code *string `json:"code"` + Explanation string `json:"explanation"` + Value int64 `json:"value"` +} + +// Resource store to contain binary objects referenced from JSON serializable structures +// +// container for binary assets (like thumbnails) +type ResourceStore struct { + BasePath *string `json:"base_path"` + Label *string `json:"label"` + Resources map[string][]int64 `json:"resources"` +} + +// A map of validation results for a manifest store. +// +// The map contains the validation results for the active manifest and any ingredient +// deltas. It is normal for there to be many +type ValidationResults struct { + ActiveManifest *StatusCodes `json:"activeManifest"` + IngredientDeltas []IngredientDeltaValidationResult `json:"ingredientDeltas"` +} + +// Contains a set of success, informational, and failure validation status codes. +// +// Validation results for the ingredient's active manifest +type StatusCodes struct { + Failure []ValidationStatus `json:"failure"` + Informational []ValidationStatus `json:"informational"` + Success []ValidationStatus `json:"success"` +} + +// A `ValidationStatus` struct describes the validation status of a specific part of a +// manifest. +// +// See +// . +type ValidationStatus struct { + Code string `json:"code"` + Explanation *string `json:"explanation"` + Success *bool `json:"success"` + URL *string `json:"url"` +} + +// Represents any changes or deltas between the current and previous validation results for +// an ingredient's manifest. +type IngredientDeltaValidationResult struct { + // JUMBF URI reference to the ingredient assertion + IngredientAssertionURI string `json:"ingredientAssertionURI"` + // Validation results for the ingredient's active manifest + ValidationDeltas StatusCodes `json:"validationDeltas"` +} + +// Use a ManifestDefinition to define a manifest and to build a `ManifestStore`. A manifest +// is a collection of ingredients and assertions used to define a claim that can be signed +// and embedded into a file. +type ManifestDefinition struct { + // A list of assertions + Assertions []AssertionDefinition `json:"assertions,omitempty"` + // Claim Generator Info is always required with at least one entry + ClaimGeneratorInfo []ClaimGeneratorInfo `json:"claim_generator_info,omitempty"` + // The version of the claim. Defaults to 1. + ClaimVersion *int64 `json:"claim_version"` + // The format of the source file as a MIME type. + Format *string `json:"format,omitempty"` + // A List of ingredients + Ingredients []Ingredient `json:"ingredients,omitempty"` + // Instance ID from `xmpMM:InstanceID` in XMP metadata. + InstanceID *string `json:"instance_id,omitempty"` + // Allows you to pre-define the manifest label, which must be unique. Not intended for + // general use. If not set, it will be assigned automatically. + Label *string `json:"label"` + // Optional manifest metadata. This will be deprecated in the future; not recommended to use. + Metadata []AssertionMetadata `json:"metadata"` + // A list of redactions - URIs to redacted assertions. + Redactions []string `json:"redactions"` + // An optional ResourceRef to a thumbnail image that represents the asset that was signed. + // Must be available when the manifest is signed. + Thumbnail *IngredientResourceRef `json:"thumbnail"` + // A human-readable title, generally source filename. + Title *string `json:"title"` + // Optional prefix added to the generated Manifest Label This is typically a reverse domain + // name. + Vendor *string `json:"vendor"` +} + +// Use a Reader to read and validate a manifest store. +type Reader struct { + // A label for the active (most recent) manifest in the store + ActiveManifest *string `json:"active_manifest"` + // A HashMap of Manifests + Manifests map[string]Manifest `json:"manifests"` + // ValidationStatus generated when loading the ManifestStore from an asset + ValidationResults *ValidationResults `json:"validation_results"` + // The validation state of the manifest store + ValidationState *ValidationState `json:"validation_state"` + // ValidationStatus generated when loading the ManifestStore from an asset + ValidationStatus []ValidationStatus `json:"validation_status"` +} + +// A Manifest represents all the information in a c2pa manifest +type Manifest struct { + // A list of assertions + Assertions []ManifestAssertion `json:"assertions,omitempty"` + // A User Agent formatted string identifying the software/hardware/system produced this + // claim Spaces are not allowed in names, versions can be specified with product/1.0 syntax. + ClaimGenerator *string `json:"claim_generator"` + // A list of claim generator info data identifying the software/hardware/system produced + // this claim. + ClaimGeneratorInfo []ClaimGeneratorInfo `json:"claim_generator_info"` + // A List of verified credentials + Credentials []interface{} `json:"credentials"` + // The format of the source file as a MIME type. + Format *string `json:"format"` + // A List of ingredients + Ingredients []Ingredient `json:"ingredients,omitempty"` + // Instance ID from `xmpMM:InstanceID` in XMP metadata. + InstanceID *string `json:"instance_id,omitempty"` + Label *string `json:"label"` + // A list of user metadata for this claim. + Metadata []AssertionMetadata `json:"metadata"` + // A list of redactions - URIs to a redacted assertions + Redactions []string `json:"redactions"` + // container for binary assets (like thumbnails) + Resources *ResourceStore `json:"resources,omitempty"` + // Signature data (only used for reporting) + SignatureInfo *SignatureInfo `json:"signature_info"` + Thumbnail *IngredientResourceRef `json:"thumbnail"` + // A human-readable title, generally source filename. + Title *string `json:"title"` + // Optional prefix added to the generated Manifest label. This is typically an internet + // domain name for the vendor (i.e. `adobe`). + Vendor *string `json:"vendor"` +} + +// A labeled container for an Assertion value in a Manifest +type ManifestAssertion struct { + // The data of the assertion as Value + Data interface{} `json:"data"` + // There can be more than one assertion for any label + Instance *int64 `json:"instance"` + // The [ManifestAssertionKind] for this assertion (as stored in c2pa content) + Kind *ManifestAssertionKind `json:"kind"` + // An assertion label in reverse domain format + Label string `json:"label"` +} + +// Holds information about a signature +type SignatureInfo struct { + // Human-readable issuing authority for this signature. + Alg *SigningAlg `json:"alg"` + // The serial number of the certificate. + CERTSerialNumber *string `json:"cert_serial_number"` + // Human-readable issuing authority for this signature. + Issuer *string `json:"issuer"` + // Revocation status of the certificate. + RevocationStatus *bool `json:"revocation_status"` + // The time the signature was created. + Time *string `json:"time"` +} + +// Media whose digital content is effectively empty, such as a blank canvas or zero-length +// video. +// +// Data that is the result of algorithmically using a model derived from sampled content and +// data. Differs from +// trainedAlgorithmicMedia in that the +// result isn’t a media type (e.g., image or video) but is a data format (e.g., CSV, pickle). +type DigitalSourceType string + +const ( + Empty DigitalSourceType = "Empty" + TrainedAlgorithmicData DigitalSourceType = "TrainedAlgorithmicData" +) + +type BuilderFlowEnum string + +const ( + Open BuilderFlowEnum = "Open" + Update BuilderFlowEnum = "Update" +) + +// The type of shape. +// +// The type of shape for the range. +// +// A rectangle. +// +// A circle. +// +// A polygon. +type ShapeType string + +const ( + Circle ShapeType = "circle" + Polygon ShapeType = "polygon" + Rectangle ShapeType = "rectangle" +) + +// The type of unit for the shape range. +// +// The type of unit for the range. +// +// Use pixels. +// +// Use percentage. +type UnitType string + +const ( + Percent UnitType = "percent" + Pixel UnitType = "pixel" +) + +// The type of time. +// +// Times are described using Normal Play Time (npt) as described in RFC 2326. +type TimeType string + +const ( + Npt TimeType = "npt" +) + +// The type of range of interest. +// +// The type of range for the region of interest. +// +// A spatial range, see [`Shape`] for more details. +// +// A temporal range, see [`Time`] for more details. +// +// A spatial range, see [`Frame`] for more details. +// +// A textual range, see [`Text`] for more details. +// +// A range identified by a specific identifier and value, see [`Item`] for more details. +type RangeType string + +const ( + Identified RangeType = "identified" + RangeTypeFrame RangeType = "frame" + Spatial RangeType = "spatial" + Temporal RangeType = "temporal" + Textual RangeType = "textual" +) + +// Arbitrary area worth identifying. +// +// This area is all that is left after a crop action. +// +// This area has had edits applied to it. +// +// The area where an ingredient was placed/added. +// +// Something in this area was redacted. +// +// Area specific to a subject (human or not). +// +// A range of information was removed/deleted. +// +// Styling was applied to this area. +// +// Invisible watermarking was applied to this area for the purpose of soft binding. +type Role string + +const ( + C2PaAreaOfInterest Role = "c2pa.areaOfInterest" + C2PaCropped Role = "c2pa.cropped" + C2PaDeleted Role = "c2pa.deleted" + C2PaEdited Role = "c2pa.edited" + C2PaPlaced Role = "c2pa.placed" + C2PaRedacted Role = "c2pa.redacted" + C2PaStyled Role = "c2pa.styled" + C2PaSubjectArea Role = "c2pa.subjectArea" + C2PaWatermarked Role = "c2pa.watermarked" +) + +// Set to `ParentOf` if this is the parent ingredient. +// +// There can only be one parent ingredient in the ingredients. +// +// The relationship of the ingredient to the current asset. +// +// The current asset is derived from this ingredient. +// +// The current asset is a part of this ingredient. +// +// The ingredient was used as an input to a computational process to create or modify the +// asset. +type Relationship string + +const ( + ComponentOf Relationship = "componentOf" + InputTo Relationship = "inputTo" + ParentOf Relationship = "parentOf" +) + +// Assertions in C2PA can be stored in several formats +type ManifestAssertionKind string + +const ( + Binary ManifestAssertionKind = "Binary" + Cbor ManifestAssertionKind = "Cbor" + JSON ManifestAssertionKind = "Json" + URI ManifestAssertionKind = "Uri" +) + +// ECDSA with SHA-256 +// +// ECDSA with SHA-256 on secp256k1 curve +// +// ECDSA with SHA-384 +// +// ECDSA with SHA-512 +// +// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 +// +// RSASSA-PSS using SHA-384 and MGF1 with SHA-384 +// +// RSASSA-PSS using SHA-512 and MGF1 with SHA-512 +// +// Edwards-Curve DSA (Ed25519 instance only) +type SigningAlg string + +const ( + Ed25519 SigningAlg = "Ed25519" + Es256 SigningAlg = "Es256" + Es256K SigningAlg = "Es256K" + Es384 SigningAlg = "Es384" + Es512 SigningAlg = "Es512" + Ps256 SigningAlg = "Ps256" + Ps384 SigningAlg = "Ps384" + Ps512 SigningAlg = "Ps512" +) + +// Errors were found in the manifest store. +// +// No errors were found in validation, but the active signature is not trusted. +// +// The manifest store is valid and the active signature is trusted. +type ValidationState string + +const ( + Invalid ValidationState = "Invalid" + Trusted ValidationState = "Trusted" + Valid ValidationState = "Valid" +) + +// The type of builder being used. +type BuilderFlowUnion struct { + BuilderFlowClass *BuilderFlowClass + Enum *BuilderFlowEnum +} + +func (x *BuilderFlowUnion) UnmarshalJSON(data []byte) error { + x.BuilderFlowClass = nil + x.Enum = nil + var c BuilderFlowClass + object, err := unmarshalUnion(data, nil, nil, nil, nil, false, nil, true, &c, false, nil, true, &x.Enum, true) + if err != nil { + return err + } + if object { + x.BuilderFlowClass = &c + } + return nil +} + +func (x *BuilderFlowUnion) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, nil, false, nil, x.BuilderFlowClass != nil, x.BuilderFlowClass, false, nil, x.Enum != nil, x.Enum, true) +} + +type BasePath struct { + IntegerArray []int64 + String *string +} + +func (x *BasePath) UnmarshalJSON(data []byte) error { + x.IntegerArray = nil + object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.IntegerArray, false, nil, false, nil, false, nil, true) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *BasePath) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, x.String, x.IntegerArray != nil, x.IntegerArray, false, nil, false, nil, false, nil, true) +} + +func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) { + if pi != nil { + *pi = nil + } + if pf != nil { + *pf = nil + } + if pb != nil { + *pb = nil + } + if ps != nil { + *ps = nil + } + + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + tok, err := dec.Token() + if err != nil { + return false, err + } + + switch v := tok.(type) { + case json.Number: + if pi != nil { + i, err := v.Int64() + if err == nil { + *pi = &i + return false, nil + } + } + if pf != nil { + f, err := v.Float64() + if err == nil { + *pf = &f + return false, nil + } + return false, errors.New("Unparsable number") + } + return false, errors.New("Union does not contain number") + case float64: + return false, errors.New("Decoder should not return float64") + case bool: + if pb != nil { + *pb = &v + return false, nil + } + return false, errors.New("Union does not contain bool") + case string: + if haveEnum { + return false, json.Unmarshal(data, pe) + } + if ps != nil { + *ps = &v + return false, nil + } + return false, errors.New("Union does not contain string") + case nil: + if nullable { + return false, nil + } + return false, errors.New("Union does not contain null") + case json.Delim: + if v == '{' { + if haveObject { + return true, json.Unmarshal(data, pc) + } + if haveMap { + return false, json.Unmarshal(data, pm) + } + return false, errors.New("Union does not contain object") + } + if v == '[' { + if haveArray { + return false, json.Unmarshal(data, pa) + } + return false, errors.New("Union does not contain array") + } + return false, errors.New("Cannot handle delimiter") + } + return false, errors.New("Cannot unmarshal union") +} + +func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) ([]byte, error) { + if pi != nil { + return json.Marshal(*pi) + } + if pf != nil { + return json.Marshal(*pf) + } + if pb != nil { + return json.Marshal(*pb) + } + if ps != nil { + return json.Marshal(*ps) + } + if haveArray { + return json.Marshal(pa) + } + if haveObject { + return json.Marshal(pc) + } + if haveMap { + return json.Marshal(pm) + } + if haveEnum { + return json.Marshal(pe) + } + if nullable { + return json.Marshal(nil) + } + return nil, errors.New("Union must not be null") +} diff --git a/pkg/c2pa/Reader.schema.json b/pkg/c2patypes/json/Everything.schema.json similarity index 68% rename from pkg/c2pa/Reader.schema.json rename to pkg/c2patypes/json/Everything.schema.json index b481d0a2..a26df468 100644 --- a/pkg/c2pa/Reader.schema.json +++ b/pkg/c2patypes/json/Everything.schema.json @@ -1,57 +1,17 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Reader", - "description": "Use a Reader to read and validate a manifest store.", + "title": "Everything", "type": "object", - "required": [ - "manifests" - ], + "required": ["builder", "manifest_definition", "reader"], "properties": { - "active_manifest": { - "description": "A label for the active (most recent) manifest in the store", - "type": [ - "string", - "null" - ] - }, - "manifests": { - "description": "A HashMap of Manifests", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Manifest" - } - }, - "validation_results": { - "description": "ValidationStatus generated when loading the ManifestStore from an asset", - "anyOf": [ - { - "$ref": "#/definitions/ValidationResults" - }, - { - "type": "null" - } - ] + "builder": { + "$ref": "#/definitions/Builder" }, - "validation_state": { - "description": "The validation state of the manifest store", - "anyOf": [ - { - "$ref": "#/definitions/ValidationState" - }, - { - "type": "null" - } - ] + "manifest_definition": { + "$ref": "#/definitions/ManifestDefinition" }, - "validation_status": { - "description": "ValidationStatus generated when loading the ManifestStore from an asset", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/ValidationStatus" - } + "reader": { + "$ref": "#/definitions/Reader" } }, "definitions": { @@ -61,20 +21,31 @@ "properties": { "credentials": { "description": "List of references to W3C Verifiable Credentials.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/HashedUri" } }, "identifier": { "description": "An identifier for a human actor, used when the \"type\" is `humanEntry.identified`.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] + } + } + }, + "AssertionData": { + "description": "This allows the assertion to be expressed as CBOR or JSON. The default is CBOR unless you specify that an assertion should be JSON.", + "anyOf": [true] + }, + "AssertionDefinition": { + "description": "Defines an assertion that consists of a label that can be either a C2PA-defined assertion label or a custom label in reverse domain format.", + "type": "object", + "required": ["data", "label"], + "properties": { + "data": { + "$ref": "#/definitions/AssertionData" + }, + "label": { + "type": "string" } } }, @@ -123,10 +94,7 @@ ] }, "reviewRatings": { - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/ReviewRating" } @@ -135,27 +103,151 @@ }, "AssetType": { "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "type": "string" }, "version": { - "type": [ - "string", - "null" + "type": ["string", "null"] + } + } + }, + "Builder": { + "description": "Use a Builder to add a signed manifest to an asset.\n\n# Example: Building and signing a manifest\n\n``` use c2pa::Result; use std::path::PathBuf;\n\nuse c2pa::{create_signer, Builder, SigningAlg}; use serde::Serialize; use serde_json::json; use tempfile::tempdir;\n\n#[derive(Serialize)] struct Test { my_tag: usize, }\n\n# fn main() -> Result<()> { #[cfg(feature = \"file_io\")] { let manifest_json = json!({ \"claim_generator_info\": [ { \"name\": \"c2pa_test\", \"version\": \"1.0.0\" } ], \"title\": \"Test_Manifest\" }).to_string();\n\nlet mut builder = Builder::from_json(&manifest_json)?; builder.add_assertion(\"org.contentauth.test\", &Test { my_tag: 42 })?;\n\nlet source = PathBuf::from(\"tests/fixtures/C.jpg\"); let dir = tempdir()?; let dest = dir.path().join(\"test_file.jpg\");\n\n// Create a ps256 signer using certs and key files. TO DO: Update example. let signcert_path = \"tests/fixtures/certs/ps256.pub\"; let pkey_path = \"tests/fixtures/certs/ps256.pem\"; let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;\n\n// embed a manifest using the signer builder.sign_file( signer.as_ref(), &source, &dest)?; } # Ok(()) # } ```", + "type": "object", + "required": ["no_embed"], + "properties": { + "assertions": { + "description": "A list of assertions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/AssertionDefinition" + } + }, + "base_path": { + "description": "Base path to search for resources.", + "type": ["string", "null"] + }, + "builder_flow": { + "description": "The type of builder being used.", + "anyOf": [ + { + "$ref": "#/definitions/BuilderFlow" + }, + { + "type": "null" + } + ] + }, + "claim_generator_info": { + "description": "Claim Generator Info is always required with at least one entry", + "default": [ + { + "name": "c2pa-rs", + "version": "0.58.0" + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/ClaimGeneratorInfo" + } + }, + "claim_version": { + "description": "The version of the claim. Defaults to 1.", + "type": ["integer", "null"], + "format": "uint8", + "minimum": 0.0 + }, + "format": { + "description": "The format of the source file as a MIME type.", + "default": "application/octet-stream", + "type": "string" + }, + "ingredients": { + "description": "A List of ingredients", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Ingredient" + } + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "default": "xmp:iid:ad854dd7-b8f6-46cf-93d5-9e0607131a38", + "type": "string" + }, + "label": { + "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", + "type": ["string", "null"] + }, + "metadata": { + "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", + "type": ["array", "null"], + "items": { + "$ref": "#/definitions/AssertionMetadata" + } + }, + "no_embed": { + "description": "If true, the manifest store will not be embedded in the asset on sign", + "type": "boolean" + }, + "redactions": { + "description": "A list of redactions - URIs to redacted assertions.", + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "remote_url": { + "description": "Optional remote URL for the manifest", + "type": ["string", "null"] + }, + "thumbnail": { + "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": ["string", "null"] + }, + "vendor": { + "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", + "type": ["string", "null"] } } }, + "BuilderFlow": { + "description": "Represents the type of builder flow being used.\n\nThis determines how the builder will be used, such as creating a new asset, opening an existing asset, or updating an existing asset.", + "oneOf": [ + { + "type": "string", + "enum": ["Open", "Update"] + }, + { + "type": "object", + "required": ["Create"], + "properties": { + "Create": { + "$ref": "#/definitions/DigitalSourceType" + } + }, + "additionalProperties": false + } + ] + }, "ClaimGeneratorInfo": { "description": "Description of the claim generator, or the software used in generating the claim.\n\nThis structure is also used for actions softwareAgent", "type": "object", - "required": [ - "name" - ], + "required": ["name"], "properties": { "icon": { "description": "hashed URI to the icon (either embedded or remote)", @@ -174,17 +266,11 @@ }, "operating_system": { "description": "A human readable string of the OS the claim generator is running on", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "version": { "description": "A human readable string of the product's version", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] } }, "additionalProperties": true @@ -192,10 +278,7 @@ "Coordinate": { "description": "An x, y coordinate used for specifying vertices in polygons.", "type": "object", - "required": [ - "x", - "y" - ], + "required": ["x", "y"], "properties": { "x": { "description": "The coordinate along the x-axis.", @@ -212,26 +295,18 @@ "DataSource": { "description": "A description of the source for assertion data", "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "actors": { "description": "A list of [`Actor`]s associated with this source.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Actor" } }, "details": { "description": "A human-readable string giving details about the source of the assertion data.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "type": { "description": "A value from among the enumerated list indicating the source of the assertion.", @@ -242,24 +317,32 @@ "DateT": { "type": "string" }, + "DigitalSourceType": { + "oneOf": [ + { + "description": "Media whose digital content is effectively empty, such as a blank canvas or zero-length video.", + "type": "string", + "enum": ["Empty"] + }, + { + "description": "Data that is the result of algorithmically using a model derived from sampled content and data. Differs from trainedAlgorithmicMedia in that the result isn’t a media type (e.g., image or video) but is a data format (e.g., CSV, pickle).", + "type": "string", + "enum": ["TrainedAlgorithmicData"] + } + ] + }, "Frame": { "description": "A frame range representing starting and ending frames or pages.\n\nIf both `start` and `end` are missing, the frame will span the entire asset.", "type": "object", "properties": { "end": { "description": "The end of the frame inclusive or the end of the asset if not present.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "int32" }, "start": { "description": "The start of the frame or the end of the asset if not present.\n\nThe first frame/page starts at 0.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "int32" } } @@ -267,17 +350,11 @@ "HashedUri": { "description": "A `HashedUri` provides a reference to content available within the same manifest store.\n\nThis is described in [§8.3, URI References], of the C2PA Technical Specification.\n\n[§8.3, URI References]: https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_uri_references", "type": "object", - "required": [ - "hash", - "url" - ], + "required": ["hash", "url"], "properties": { "alg": { "description": "A string identifying the cryptographic hash algorithm used to compute the hash", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "hash": { "description": "Byte string containing the hash value", @@ -300,10 +377,7 @@ "properties": { "active_manifest": { "description": "The active manifest label (if one exists).\n\nIf this ingredient has a [`ManifestStore`], this will hold the label of the active [`Manifest`].\n\n[`Manifest`]: crate::Manifest [`ManifestStore`]: crate::ManifestStore", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "data": { "description": "A reference to the actual data of the ingredient.", @@ -318,62 +392,38 @@ }, "data_types": { "description": "Additional information about the data's type to the ingredient V2 structure.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/AssetType" } }, "description": { "description": "Additional description of the ingredient.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "document_id": { "description": "Document ID from `xmpMM:DocumentID` in XMP metadata.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "format": { "description": "The format of the source file as a MIME type.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "hash": { "description": "An optional hash of the asset to prevent duplicates.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "informational_URI": { "description": "URI to an informational page about the ingredient or its data.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "instance_id": { "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "label": { "description": "The ingredient's label as assigned in the manifest.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "manifest_data": { "description": "A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.\n\n[`ManifestStore`]: crate::ManifestStore", @@ -399,10 +449,7 @@ }, "provenance": { "description": "URI from `dcterms:provenance` in XMP metadata.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "relationship": { "description": "Set to `ParentOf` if this is the parent ingredient.\n\nThere can only be one parent ingredient in the ingredients.", @@ -434,10 +481,7 @@ }, "title": { "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "validation_results": { "description": "Validation results (Ingredient.V3)", @@ -452,10 +496,7 @@ }, "validation_status": { "description": "Validation status (Ingredient v1 & v2)", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/ValidationStatus" } @@ -465,10 +506,7 @@ "IngredientDeltaValidationResult": { "description": "Represents any changes or deltas between the current and previous validation results for an ingredient's manifest.", "type": "object", - "required": [ - "ingredientAssertionURI", - "validationDeltas" - ], + "required": ["ingredientAssertionURI", "validationDeltas"], "properties": { "ingredientAssertionURI": { "description": "JUMBF URI reference to the ingredient assertion", @@ -487,10 +525,7 @@ "Item": { "description": "Description of the boundaries of an identified range.", "type": "object", - "required": [ - "identifier", - "value" - ], + "required": ["identifier", "value"], "properties": { "identifier": { "description": "The container-specific term used to identify items, such as \"track_id\" for MP4 or \"item_ID\" for HEIF.", @@ -516,35 +551,23 @@ }, "claim_generator": { "description": "A User Agent formatted string identifying the software/hardware/system produced this claim Spaces are not allowed in names, versions can be specified with product/1.0 syntax.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "claim_generator_info": { "description": "A list of claim generator info data identifying the software/hardware/system produced this claim.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/ClaimGeneratorInfo" } }, "credentials": { "description": "A List of verified credentials", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": true }, "format": { "description": "The format of the source file as a MIME type.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "ingredients": { "description": "A List of ingredients", @@ -556,31 +579,22 @@ }, "instance_id": { "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", - "default": "xmp:iid:855e296c-2f54-4a0a-ae5a-0659b3bca8b8", + "default": "xmp:iid:162c4576-2f5c-4852-93ad-e15b784e19e8", "type": "string" }, "label": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "metadata": { "description": "A list of user metadata for this claim.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/AssertionMetadata" } }, "redactions": { "description": "A list of redactions - URIs to a redacted assertions", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "type": "string" } @@ -617,27 +631,18 @@ }, "title": { "description": "A human-readable title, generally source filename.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "vendor": { "description": "Optional prefix added to the generated Manifest label. This is typically an internet domain name for the vendor (i.e. `adobe`).", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] } } }, "ManifestAssertion": { "description": "A labeled container for an Assertion value in a Manifest", "type": "object", - "required": [ - "data", - "label" - ], + "required": ["data", "label"], "properties": { "data": { "description": "The data of the assertion as Value", @@ -649,10 +654,7 @@ }, "instance": { "description": "There can be more than one assertion for any label", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "uint", "minimum": 0.0 }, @@ -676,12 +678,7 @@ "ManifestAssertionKind": { "description": "Assertions in C2PA can be stored in several formats", "type": "string", - "enum": [ - "Cbor", - "Json", - "Binary", - "Uri" - ] + "enum": ["Cbor", "Json", "Binary", "Uri"] }, "ManifestData": { "anyOf": [ @@ -696,12 +693,98 @@ } ] }, + "ManifestDefinition": { + "description": "Use a ManifestDefinition to define a manifest and to build a `ManifestStore`. A manifest is a collection of ingredients and assertions used to define a claim that can be signed and embedded into a file.", + "type": "object", + "properties": { + "assertions": { + "description": "A list of assertions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/AssertionDefinition" + } + }, + "claim_generator_info": { + "description": "Claim Generator Info is always required with at least one entry", + "default": [ + { + "name": "c2pa-rs", + "version": "0.58.0" + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/ClaimGeneratorInfo" + } + }, + "claim_version": { + "description": "The version of the claim. Defaults to 1.", + "type": ["integer", "null"], + "format": "uint8", + "minimum": 0.0 + }, + "format": { + "description": "The format of the source file as a MIME type.", + "default": "application/octet-stream", + "type": "string" + }, + "ingredients": { + "description": "A List of ingredients", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Ingredient" + } + }, + "instance_id": { + "description": "Instance ID from `xmpMM:InstanceID` in XMP metadata.", + "default": "xmp:iid:39745b0f-82b0-4848-b2ac-695b1acd4893", + "type": "string" + }, + "label": { + "description": "Allows you to pre-define the manifest label, which must be unique. Not intended for general use. If not set, it will be assigned automatically.", + "type": ["string", "null"] + }, + "metadata": { + "description": "Optional manifest metadata. This will be deprecated in the future; not recommended to use.", + "type": ["array", "null"], + "items": { + "$ref": "#/definitions/AssertionMetadata" + } + }, + "redactions": { + "description": "A list of redactions - URIs to redacted assertions.", + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "thumbnail": { + "description": "An optional ResourceRef to a thumbnail image that represents the asset that was signed. Must be available when the manifest is signed.", + "anyOf": [ + { + "$ref": "#/definitions/ResourceRef" + }, + { + "type": "null" + } + ] + }, + "title": { + "description": "A human-readable title, generally source filename.", + "type": ["string", "null"] + }, + "vendor": { + "description": "Optional prefix added to the generated Manifest Label This is typically a reverse domain name.", + "type": ["string", "null"] + } + } + }, "Range": { "description": "A spatial, temporal, frame, or textual range describing the region of interest.", "type": "object", - "required": [ - "type" - ], + "required": ["type"], "properties": { "frame": { "description": "A frame range.", @@ -774,60 +857,89 @@ { "description": "A spatial range, see [`Shape`] for more details.", "type": "string", - "enum": [ - "spatial" - ] + "enum": ["spatial"] }, { "description": "A temporal range, see [`Time`] for more details.", "type": "string", - "enum": [ - "temporal" - ] + "enum": ["temporal"] }, { "description": "A spatial range, see [`Frame`] for more details.", "type": "string", - "enum": [ - "frame" - ] + "enum": ["frame"] }, { "description": "A textual range, see [`Text`] for more details.", "type": "string", - "enum": [ - "textual" - ] + "enum": ["textual"] }, { "description": "A range identified by a specific identifier and value, see [`Item`] for more details.", "type": "string", - "enum": [ - "identified" - ] + "enum": ["identified"] } ] }, + "Reader": { + "description": "Use a Reader to read and validate a manifest store.", + "type": "object", + "required": ["manifests"], + "properties": { + "active_manifest": { + "description": "A label for the active (most recent) manifest in the store", + "type": ["string", "null"] + }, + "manifests": { + "description": "A HashMap of Manifests", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Manifest" + } + }, + "validation_results": { + "description": "ValidationStatus generated when loading the ManifestStore from an asset", + "anyOf": [ + { + "$ref": "#/definitions/ValidationResults" + }, + { + "type": "null" + } + ] + }, + "validation_state": { + "description": "The validation state of the manifest store", + "anyOf": [ + { + "$ref": "#/definitions/ValidationState" + }, + { + "type": "null" + } + ] + }, + "validation_status": { + "description": "ValidationStatus generated when loading the ManifestStore from an asset", + "type": ["array", "null"], + "items": { + "$ref": "#/definitions/ValidationStatus" + } + } + } + }, "RegionOfInterest": { "description": "A region of interest within an asset describing the change.\n\nThis struct can be used from [`Action::changes`][crate::assertions::Action::changes] or [`AssertionMetadata::region_of_interest`][crate::assertions::AssertionMetadata::region_of_interest].", "type": "object", - "required": [ - "region" - ], + "required": ["region"], "properties": { "description": { "description": "A free-text string.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "identifier": { "description": "A free-text string representing a machine-readable, unique to this assertion, identifier for the region.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "metadata": { "description": "Additional information about the asset.", @@ -842,10 +954,7 @@ }, "name": { "description": "A free-text string representing a human-readable name for the region which might be used in a user interface.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "region": { "description": "A range describing the region of interest for the specific asset.", @@ -867,10 +976,7 @@ }, "type": { "description": "A value from a controlled vocabulary such as or an entity-specific value (e.g., com.litware.newType) that represents the type of thing(s) depicted by a region.\n\nNote this field serializes/deserializes into the name `type`.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] } } }, @@ -880,47 +986,32 @@ { "description": "The current asset is derived from this ingredient.", "type": "string", - "enum": [ - "parentOf" - ] + "enum": ["parentOf"] }, { "description": "The current asset is a part of this ingredient.", "type": "string", - "enum": [ - "componentOf" - ] + "enum": ["componentOf"] }, { "description": "The ingredient was used as an input to a computational process to create or modify the asset.", "type": "string", - "enum": [ - "inputTo" - ] + "enum": ["inputTo"] } ] }, "ResourceRef": { "description": "A reference to a resource to be used in JSON serialization.\n\nThe underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].", "type": "object", - "required": [ - "format", - "identifier" - ], + "required": ["format", "identifier"], "properties": { "alg": { "description": "The algorithm used to hash the resource (if applicable).", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "data_types": { "description": "More detailed data types as defined in the C2PA spec.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/AssetType" } @@ -931,10 +1022,7 @@ }, "hash": { "description": "The hash of the resource (if applicable).", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "identifier": { "description": "A URI that identifies the resource as referenced from the manifest.\n\nThis may be a JUMBF URI, a file path, a URL or any other string. Relative JUMBF URIs will be resolved with the manifest label. Relative file paths will be resolved with the base path if provided.", @@ -945,21 +1033,13 @@ "ResourceStore": { "description": "Resource store to contain binary objects referenced from JSON serializable structures", "type": "object", - "required": [ - "resources" - ], + "required": ["resources"], "properties": { "base_path": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "label": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "resources": { "type": "object", @@ -977,16 +1057,10 @@ "ReviewRating": { "description": "A rating on an Assertion.\n\nSee .", "type": "object", - "required": [ - "explanation", - "value" - ], + "required": ["explanation", "value"], "properties": { "code": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "explanation": { "type": "string" @@ -1004,91 +1078,63 @@ { "description": "Arbitrary area worth identifying.", "type": "string", - "enum": [ - "c2pa.areaOfInterest" - ] + "enum": ["c2pa.areaOfInterest"] }, { "description": "This area is all that is left after a crop action.", "type": "string", - "enum": [ - "c2pa.cropped" - ] + "enum": ["c2pa.cropped"] }, { "description": "This area has had edits applied to it.", "type": "string", - "enum": [ - "c2pa.edited" - ] + "enum": ["c2pa.edited"] }, { "description": "The area where an ingredient was placed/added.", "type": "string", - "enum": [ - "c2pa.placed" - ] + "enum": ["c2pa.placed"] }, { "description": "Something in this area was redacted.", "type": "string", - "enum": [ - "c2pa.redacted" - ] + "enum": ["c2pa.redacted"] }, { "description": "Area specific to a subject (human or not).", "type": "string", - "enum": [ - "c2pa.subjectArea" - ] + "enum": ["c2pa.subjectArea"] }, { "description": "A range of information was removed/deleted.", "type": "string", - "enum": [ - "c2pa.deleted" - ] + "enum": ["c2pa.deleted"] }, { "description": "Styling was applied to this area.", "type": "string", - "enum": [ - "c2pa.styled" - ] + "enum": ["c2pa.styled"] }, { "description": "Invisible watermarking was applied to this area for the purpose of soft binding.", "type": "string", - "enum": [ - "c2pa.watermarked" - ] + "enum": ["c2pa.watermarked"] } ] }, "Shape": { "description": "A spatial range representing rectangle, circle, or a polygon.", "type": "object", - "required": [ - "origin", - "type", - "unit" - ], + "required": ["origin", "type", "unit"], "properties": { "height": { "description": "The height of a rectnagle.\n\nThis field can be ignored for circles and polygons.", - "type": [ - "number", - "null" - ], + "type": ["number", "null"], "format": "double" }, "inside": { "description": "If the range is inside the shape.\n\nThe default value is true.", - "type": [ - "boolean", - "null" - ] + "type": ["boolean", "null"] }, "origin": { "description": "THe origin of the coordinate in the shape.", @@ -1116,20 +1162,14 @@ }, "vertices": { "description": "The vertices of the polygon.\n\nThis field can be ignored for rectangles and circles.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Coordinate" } }, "width": { "description": "The width for rectangles or diameter for circles.\n\nThis field can be ignored for polygons.", - "type": [ - "number", - "null" - ], + "type": ["number", "null"], "format": "double" } } @@ -1140,23 +1180,17 @@ { "description": "A rectangle.", "type": "string", - "enum": [ - "rectangle" - ] + "enum": ["rectangle"] }, { "description": "A circle.", "type": "string", - "enum": [ - "circle" - ] + "enum": ["circle"] }, { "description": "A polygon.", "type": "string", - "enum": [ - "polygon" - ] + "enum": ["polygon"] } ] }, @@ -1177,31 +1211,19 @@ }, "cert_serial_number": { "description": "The serial number of the certificate.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "issuer": { "description": "Human-readable issuing authority for this signature.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "revocation_status": { "description": "Revocation status of the certificate.", - "type": [ - "boolean", - "null" - ] + "type": ["boolean", "null"] }, "time": { "description": "The time the signature was created.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] } } }, @@ -1211,69 +1233,49 @@ { "description": "ECDSA with SHA-256", "type": "string", - "enum": [ - "Es256" - ] + "enum": ["Es256"] }, { "description": "ECDSA with SHA-256 on secp256k1 curve", "type": "string", - "enum": [ - "Es256K" - ] + "enum": ["Es256K"] }, { "description": "ECDSA with SHA-384", "type": "string", - "enum": [ - "Es384" - ] + "enum": ["Es384"] }, { "description": "ECDSA with SHA-512", "type": "string", - "enum": [ - "Es512" - ] + "enum": ["Es512"] }, { "description": "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "type": "string", - "enum": [ - "Ps256" - ] + "enum": ["Ps256"] }, { "description": "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "type": "string", - "enum": [ - "Ps384" - ] + "enum": ["Ps384"] }, { "description": "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "type": "string", - "enum": [ - "Ps512" - ] + "enum": ["Ps512"] }, { "description": "Edwards-Curve DSA (Ed25519 instance only)", "type": "string", - "enum": [ - "Ed25519" - ] + "enum": ["Ed25519"] } ] }, "StatusCodes": { "description": "Contains a set of success, informational, and failure validation status codes.", "type": "object", - "required": [ - "failure", - "informational", - "success" - ], + "required": ["failure", "informational", "success"], "properties": { "failure": { "type": "array", @@ -1298,9 +1300,7 @@ "Text": { "description": "A textual range representing multiple (possibly discontinuous) ranges of text.", "type": "object", - "required": [ - "selectors" - ], + "required": ["selectors"], "properties": { "selectors": { "description": "The ranges of text to select.", @@ -1314,16 +1314,11 @@ "TextSelector": { "description": "Selects a range of text via a fragment identifier.\n\nThis is modeled after the W3C Web Annotation selector model.", "type": "object", - "required": [ - "fragment" - ], + "required": ["fragment"], "properties": { "end": { "description": "The end character offset or the end of the fragment if not present.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "int32" }, "fragment": { @@ -1332,10 +1327,7 @@ }, "start": { "description": "The start character offset or the start of the fragment if not present.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "int32" } } @@ -1343,9 +1335,7 @@ "TextSelectorRange": { "description": "One or two [`TextSelector`][TextSelector] identifiying the range to select.", "type": "object", - "required": [ - "selector" - ], + "required": ["selector"], "properties": { "end": { "description": "The end of the text range.", @@ -1374,17 +1364,11 @@ "properties": { "end": { "description": "The end time or the end of the asset if not present.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "start": { "description": "The start time or the start of the asset if not present.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "type": { "description": "The type of time.", @@ -1403,9 +1387,7 @@ { "description": "Times are described using Normal Play Time (npt) as described in RFC 2326.", "type": "string", - "enum": [ - "npt" - ] + "enum": ["npt"] } ] }, @@ -1415,16 +1397,12 @@ { "description": "Use pixels.", "type": "string", - "enum": [ - "pixel" - ] + "enum": ["pixel"] }, { "description": "Use percentage.", "type": "string", - "enum": [ - "percent" - ] + "enum": ["percent"] } ] }, @@ -1453,10 +1431,7 @@ ] }, "ingredientDeltas": { - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/IngredientDeltaValidationResult" } @@ -1469,56 +1444,39 @@ { "description": "Errors were found in the manifest store.", "type": "string", - "enum": [ - "Invalid" - ] + "enum": ["Invalid"] }, { "description": "No errors were found in validation, but the active signature is not trusted.", "type": "string", - "enum": [ - "Valid" - ] + "enum": ["Valid"] }, { "description": "The manifest store is valid and the active signature is trusted.", "type": "string", - "enum": [ - "Trusted" - ] + "enum": ["Trusted"] } ] }, "ValidationStatus": { "description": "A `ValidationStatus` struct describes the validation status of a specific part of a manifest.\n\nSee .", "type": "object", - "required": [ - "code" - ], + "required": ["code"], "properties": { "code": { "type": "string" }, "explanation": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "success": { "writeOnly": true, - "type": [ - "boolean", - "null" - ] + "type": ["boolean", "null"] }, "url": { - "type": [ - "string", - "null" - ] + "type": ["string", "null"] } } } } -} \ No newline at end of file +} diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index b85bf81d..dde41e71 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -3,6 +3,7 @@ package cmd import ( "context" "crypto" + "encoding/json" "errors" "flag" "fmt" @@ -24,6 +25,7 @@ import ( "stream.place/streamplace/pkg/aqhttp" "stream.place/streamplace/pkg/atproto" "stream.place/streamplace/pkg/bus" + c2patypes "stream.place/streamplace/pkg/c2patypes" "stream.place/streamplace/pkg/crypto/signers" "stream.place/streamplace/pkg/crypto/signers/eip712" "stream.place/streamplace/pkg/director" @@ -56,8 +58,17 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { if err != nil { return err } - cert, _ := iroh_streamplace.PrintCert(bs) - panic(cert) + manifest, rustErr := iroh_streamplace.GetManifest(bs) + if rustErr.AsError() != nil { + return rustErr.AsError() + } + var mani c2patypes.Manifest + err = json.Unmarshal([]byte(manifest), &mani) + if err != nil { + return err + } + fmt.Println(mani) + os.Exit(0) selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" err = media.RunSelfTest(context.Background()) if err != nil { diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index 75b63471..2d636ae9 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -349,6 +349,15 @@ func uniffiCheckChecksums() { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: UniFFI contract version mismatch") } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_func_get_manifest() + }) + if checksum != 65147 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_get_manifest: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_func_print_cert() @@ -2111,6 +2120,20 @@ func iroh_streamplace_uniffiFreeGorutine(data C.uint64_t) { guard <- struct{}{} } +func GetManifest(data []byte) (string, *CertError) { + _uniffiRV, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_func_get_manifest(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + } + }) + if _uniffiErr != nil { + var _uniffiDefaultValue string + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterStringINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} + func PrintCert(data []byte) (string, *CertError) { _uniffiRV, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { return GoRustBuffer{ diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 02ca4d6c..a8bee0e8 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -571,6 +571,11 @@ uint64_t uniffi_iroh_streamplace_fn_method_sender_node_addr(void* ptr uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key, RustBuffer data ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST +RustBuffer uniffi_iroh_streamplace_fn_func_get_manifest(RustBuffer data, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT RustBuffer uniffi_iroh_streamplace_fn_func_print_cert(RustBuffer data, RustCallStatus *out_status @@ -854,6 +859,12 @@ void ffi_iroh_streamplace_rust_future_free_void(uint64_t handle #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID #define UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID void ffi_iroh_streamplace_rust_future_complete_void(uint64_t handle, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_GET_MANIFEST +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_GET_MANIFEST +uint16_t uniffi_iroh_streamplace_checksum_func_get_manifest(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_PRINT_CERT diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs index 8d505e6e..3207d743 100644 --- a/rust/iroh-streamplace/src/c2pa.rs +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -24,3 +24,14 @@ pub fn print_cert(data: Vec) -> Result { } Err(CertError::NoCertificateChainFound) } + +#[uniffi::export] +pub fn get_manifest(data: Vec) -> Result { + let reader = Reader::from_stream("video/mp4", Cursor::new(data)) + .map_err(|e| CertError::C2paError(e.to_string()))?; + // todo: add cawg certs here?? + if let Some(manifest) = reader.active_manifest() { + return Ok(manifest.to_string()); + } + Err(CertError::NoCertificateChainFound) +} -- 2.51.2 From f761bc255ce866597610698674673ff0965168c6 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 23 Sep 2025 17:19:07 -0700 Subject: [PATCH 036/525] c2pa: basic, hacky callback signer --- pkg/api/stream_key.go | 6 +- pkg/c2patypes/callback_signer.go | 17 + pkg/cmd/streamplace.go | 37 +- pkg/crypto/signers/signers.go | 8 +- .../iroh_streamplace/iroh_streamplace.go | 418 ++++++++++++------ .../iroh_streamplace/iroh_streamplace.h | 73 ++- pkg/media/media.go | 6 +- pkg/media/media_signer.go | 82 ++-- pkg/media/media_signer_ext.go | 232 +++++----- pkg/media/validate.go | 25 +- rust/iroh-streamplace/src/c2pa.rs | 122 ++++- 11 files changed, 660 insertions(+), 366 deletions(-) create mode 100644 pkg/c2patypes/callback_signer.go diff --git a/pkg/api/stream_key.go b/pkg/api/stream_key.go index f48cc083..4b0e45ae 100644 --- a/pkg/api/stream_key.go +++ b/pkg/api/stream_key.go @@ -83,11 +83,7 @@ func (a *StreamplaceAPI) MakeMediaSigner(ctx context.Context, keyStr string) (me } var mediaSigner media.MediaSigner - if a.CLI.ExternalSigning { - mediaSigner, err = media.MakeMediaSignerExt(ctx, a.CLI, did, addrBytes) - } else { - mediaSigner, err = media.MakeMediaSigner(ctx, a.CLI, did, signer) - } + mediaSigner, err = media.MakeMediaSigner(ctx, a.CLI, did, signer) if err != nil { return nil, fmt.Errorf("invalid authorization key (not valid secp256k1): %w", err) } diff --git a/pkg/c2patypes/callback_signer.go b/pkg/c2patypes/callback_signer.go new file mode 100644 index 00000000..93a6fc71 --- /dev/null +++ b/pkg/c2patypes/callback_signer.go @@ -0,0 +1,17 @@ +package c2patypes + +import ( + "crypto" + "crypto/rand" + "crypto/sha256" +) + +type CallbackSigner struct { + signer crypto.Signer +} + +func (c *CallbackSigner) Sign(data []byte) ([]byte, error) { + digest := sha256.New().Sum(data) + + return c.signer.Sign(rand.Reader, digest, nil) +} diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index dde41e71..d3ad3589 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -3,7 +3,6 @@ package cmd import ( "context" "crypto" - "encoding/json" "errors" "flag" "fmt" @@ -25,7 +24,6 @@ import ( "stream.place/streamplace/pkg/aqhttp" "stream.place/streamplace/pkg/atproto" "stream.place/streamplace/pkg/bus" - c2patypes "stream.place/streamplace/pkg/c2patypes" "stream.place/streamplace/pkg/crypto/signers" "stream.place/streamplace/pkg/crypto/signers/eip712" "stream.place/streamplace/pkg/director" @@ -45,8 +43,6 @@ import ( "stream.place/streamplace/pkg/api" "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/model" - - "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" ) // Additional jobs that can be injected by platforms @@ -54,23 +50,24 @@ type jobFunc func(ctx context.Context, cli *config.CLI) error // parse the CLI and fire up an streamplace node! func start(build *config.BuildFlags, platformJobs []jobFunc) error { - bs, err := os.ReadFile("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") - if err != nil { - return err - } - manifest, rustErr := iroh_streamplace.GetManifest(bs) - if rustErr.AsError() != nil { - return rustErr.AsError() - } - var mani c2patypes.Manifest - err = json.Unmarshal([]byte(manifest), &mani) - if err != nil { - return err - } - fmt.Println(mani) - os.Exit(0) + // builder := c2patypes.Builder{} + // bs, err := os.ReadFile("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") + // if err != nil { + // return err + // } + // manifest, rustErr := iroh_streamplace.GetManifest(bs) + // if rustErr.AsError() != nil { + // return rustErr.AsError() + // } + // var mani c2patypes.Manifest + // err = json.Unmarshal([]byte(manifest), &mani) + // if err != nil { + // return err + // } + // fmt.Println(mani) + // os.Exit(0) selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" - err = media.RunSelfTest(context.Background()) + err := media.RunSelfTest(context.Background()) if err != nil { if selfTest { fmt.Println(err.Error()) diff --git a/pkg/crypto/signers/signers.go b/pkg/crypto/signers/signers.go index 44efc274..7722324e 100644 --- a/pkg/crypto/signers/signers.go +++ b/pkg/crypto/signers/signers.go @@ -42,14 +42,18 @@ func GenerateES256KCert(signer gocrypto.Signer) ([]byte, error) { // publicKeyBytes := elliptic.Marshal(elliptic.P256(), pub.X, pub.Y) pub := signer.Public().(*ecdsa.PublicKey) publicKeyBytes := elliptic.Marshal(crypto.S256(), pub.X, pub.Y) //nolint:all + atpub, err := atcrypto.ParsePublicUncompressedBytesK256(publicKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } idhash := sha1.Sum(publicKeyBytes) subjectKeyID := idhash[:] - hex := HexAddr(pub) template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - CommonName: hex, + CommonName: atpub.DIDKey(), + Organization: []string{"Streamplace"}, }, NotBefore: notBefore, NotAfter: notAfter, diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index 2d636ae9..d3296b4f 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -335,6 +335,7 @@ func readFloat64(reader io.Reader) float64 { func init() { FfiConverterDataHandlerINSTANCE.register() + FfiConverterGoSignerINSTANCE.register() uniffiCheckChecksums() } @@ -349,22 +350,31 @@ func uniffiCheckChecksums() { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: UniFFI contract version mismatch") } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_func_get_cert() + }) + if checksum != 59172 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_get_cert: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_func_get_manifest() }) - if checksum != 65147 { + if checksum != 23005 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_get_manifest: UniFFI API checksum mismatch") } } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_func_print_cert() + return C.uniffi_iroh_streamplace_checksum_func_sign() }) - if checksum != 39427 { + if checksum != 23786 { // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_print_cert: UniFFI API checksum mismatch") + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_sign: UniFFI API checksum mismatch") } } { @@ -385,6 +395,15 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_endpoint_node_addr: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_gosigner_sign() + }) + if checksum != 50597 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_gosigner_sign: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_nodeaddr_direct_addresses() @@ -1041,6 +1060,135 @@ func (_ FfiDestroyerEndpoint) Destroy(value *Endpoint) { value.Destroy() } +type GoSigner interface { + Sign(data []byte) ([]byte, *SpError) +} +type GoSignerImpl struct { + ffiObject FfiObject +} + +func (_self *GoSignerImpl) Sign(data []byte) ([]byte, *SpError) { + _pointer := _self.ffiObject.incrementPointer("GoSigner") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_gosigner_sign( + _pointer, FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + } + }) + if _uniffiErr != nil { + var _uniffiDefaultValue []byte + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterBytesINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} +func (object *GoSignerImpl) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterGoSigner struct { + handleMap *concurrentHandleMap[GoSigner] +} + +var FfiConverterGoSignerINSTANCE = FfiConverterGoSigner{ + handleMap: newConcurrentHandleMap[GoSigner](), +} + +func (c FfiConverterGoSigner) Lift(pointer unsafe.Pointer) GoSigner { + result := &GoSignerImpl{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_gosigner(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_gosigner(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*GoSignerImpl).Destroy) + return result +} + +func (c FfiConverterGoSigner) Read(reader io.Reader) GoSigner { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterGoSigner) Lower(value GoSigner) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := unsafe.Pointer(uintptr(c.handleMap.insert(value))) + return pointer + +} + +func (c FfiConverterGoSigner) Write(writer io.Writer, value GoSigner) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerGoSigner struct{} + +func (_ FfiDestroyerGoSigner) Destroy(value GoSigner) { + if val, ok := value.(*GoSignerImpl); ok { + val.Destroy() + } else { + panic("Expected *GoSignerImpl") + } +} + +//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0 +func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uniffiHandle C.uint64_t, data C.RustBuffer, uniffiOutReturn *C.RustBuffer, callStatus *C.RustCallStatus) { + handle := uint64(uniffiHandle) + uniffiObj, ok := FfiConverterGoSignerINSTANCE.handleMap.tryGet(handle) + if !ok { + panic(fmt.Errorf("no callback in handle map: %d", handle)) + } + + res, err := + uniffiObj.Sign( + FfiConverterBytesINSTANCE.Lift(GoRustBuffer{ + inner: data, + }), + ) + + if err != nil { + // The only way to bypass an unexpected error is to bypass pointer to an empty + // instance of the error + if err.err == nil { + *callStatus = C.RustCallStatus{ + code: C.int8_t(uniffiCallbackUnexpectedResultError), + } + return + } + + *callStatus = C.RustCallStatus{ + code: C.int8_t(uniffiCallbackResultError), + errorBuf: FfiConverterSpErrorINSTANCE.Lower(err), + } + return + } + + *uniffiOutReturn = FfiConverterBytesINSTANCE.Lower(res) +} + +var UniffiVTableCallbackInterfaceGoSignerINSTANCE = C.UniffiVTableCallbackInterfaceGoSigner{ + sign: (C.UniffiCallbackInterfaceGoSignerMethod0)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0), + + uniffiFree: (C.UniffiCallbackInterfaceFree)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree), +} + +//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree +func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(handle C.uint64_t) { + FfiConverterGoSignerINSTANCE.handleMap.remove(uint64(handle)) +} + +func (c FfiConverterGoSigner) register() { + C.uniffi_iroh_streamplace_fn_init_callback_vtable_gosigner(&UniffiVTableCallbackInterfaceGoSignerINSTANCE) +} + // A peer and it's addressing information. type NodeAddrInterface interface { // Get the direct addresses of this peer. @@ -1610,124 +1758,6 @@ func (_ FfiDestroyerSender) Destroy(value *Sender) { value.Destroy() } -type CertError struct { - err error -} - -// Convience method to turn *CertError into error -// Avoiding treating nil pointer as non nil error interface -func (err *CertError) AsError() error { - if err == nil { - return nil - } else { - return err - } -} - -func (err CertError) Error() string { - return fmt.Sprintf("CertError: %s", err.err.Error()) -} - -func (err CertError) Unwrap() error { - return err.err -} - -// Err* are used for checking error type with `errors.Is` -var ErrCertErrorNoCertificateChainFound = fmt.Errorf("CertErrorNoCertificateChainFound") -var ErrCertErrorC2paError = fmt.Errorf("CertErrorC2paError") - -// Variant structs -type CertErrorNoCertificateChainFound struct { - message string -} - -func NewCertErrorNoCertificateChainFound() *CertError { - return &CertError{err: &CertErrorNoCertificateChainFound{}} -} - -func (e CertErrorNoCertificateChainFound) destroy() { -} - -func (err CertErrorNoCertificateChainFound) Error() string { - return fmt.Sprintf("NoCertificateChainFound: %s", err.message) -} - -func (self CertErrorNoCertificateChainFound) Is(target error) bool { - return target == ErrCertErrorNoCertificateChainFound -} - -type CertErrorC2paError struct { - message string -} - -func NewCertErrorC2paError() *CertError { - return &CertError{err: &CertErrorC2paError{}} -} - -func (e CertErrorC2paError) destroy() { -} - -func (err CertErrorC2paError) Error() string { - return fmt.Sprintf("C2paError: %s", err.message) -} - -func (self CertErrorC2paError) Is(target error) bool { - return target == ErrCertErrorC2paError -} - -type FfiConverterCertError struct{} - -var FfiConverterCertErrorINSTANCE = FfiConverterCertError{} - -func (c FfiConverterCertError) Lift(eb RustBufferI) *CertError { - return LiftFromRustBuffer[*CertError](c, eb) -} - -func (c FfiConverterCertError) Lower(value *CertError) C.RustBuffer { - return LowerIntoRustBuffer[*CertError](c, value) -} - -func (c FfiConverterCertError) Read(reader io.Reader) *CertError { - errorID := readUint32(reader) - - message := FfiConverterStringINSTANCE.Read(reader) - switch errorID { - case 1: - return &CertError{&CertErrorNoCertificateChainFound{message}} - case 2: - return &CertError{&CertErrorC2paError{message}} - default: - panic(fmt.Sprintf("Unknown error code %d in FfiConverterCertError.Read()", errorID)) - } - -} - -func (c FfiConverterCertError) Write(writer io.Writer, value *CertError) { - switch variantValue := value.err.(type) { - case *CertErrorNoCertificateChainFound: - writeInt32(writer, 1) - case *CertErrorC2paError: - writeInt32(writer, 2) - default: - _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiConverterCertError.Write", value)) - } -} - -type FfiDestroyerCertError struct{} - -func (_ FfiDestroyerCertError) Destroy(value *CertError) { - switch variantValue := value.err.(type) { - case CertErrorNoCertificateChainFound: - variantValue.destroy() - case CertErrorC2paError: - variantValue.destroy() - default: - _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerCertError.Destroy", value)) - } -} - // An Error. type Error struct { err error @@ -1977,6 +2007,124 @@ func (_ FfiDestroyerError) Destroy(value *Error) { } } +type SpError struct { + err error +} + +// Convience method to turn *SpError into error +// Avoiding treating nil pointer as non nil error interface +func (err *SpError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err SpError) Error() string { + return fmt.Sprintf("SpError: %s", err.err.Error()) +} + +func (err SpError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrSpErrorNoCertificateChainFound = fmt.Errorf("SpErrorNoCertificateChainFound") +var ErrSpErrorC2paError = fmt.Errorf("SpErrorC2paError") + +// Variant structs +type SpErrorNoCertificateChainFound struct { + message string +} + +func NewSpErrorNoCertificateChainFound() *SpError { + return &SpError{err: &SpErrorNoCertificateChainFound{}} +} + +func (e SpErrorNoCertificateChainFound) destroy() { +} + +func (err SpErrorNoCertificateChainFound) Error() string { + return fmt.Sprintf("NoCertificateChainFound: %s", err.message) +} + +func (self SpErrorNoCertificateChainFound) Is(target error) bool { + return target == ErrSpErrorNoCertificateChainFound +} + +type SpErrorC2paError struct { + message string +} + +func NewSpErrorC2paError() *SpError { + return &SpError{err: &SpErrorC2paError{}} +} + +func (e SpErrorC2paError) destroy() { +} + +func (err SpErrorC2paError) Error() string { + return fmt.Sprintf("C2paError: %s", err.message) +} + +func (self SpErrorC2paError) Is(target error) bool { + return target == ErrSpErrorC2paError +} + +type FfiConverterSpError struct{} + +var FfiConverterSpErrorINSTANCE = FfiConverterSpError{} + +func (c FfiConverterSpError) Lift(eb RustBufferI) *SpError { + return LiftFromRustBuffer[*SpError](c, eb) +} + +func (c FfiConverterSpError) Lower(value *SpError) C.RustBuffer { + return LowerIntoRustBuffer[*SpError](c, value) +} + +func (c FfiConverterSpError) Read(reader io.Reader) *SpError { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &SpError{&SpErrorNoCertificateChainFound{message}} + case 2: + return &SpError{&SpErrorC2paError{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterSpError.Read()", errorID)) + } + +} + +func (c FfiConverterSpError) Write(writer io.Writer, value *SpError) { + switch variantValue := value.err.(type) { + case *SpErrorNoCertificateChainFound: + writeInt32(writer, 1) + case *SpErrorC2paError: + writeInt32(writer, 2) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterSpError.Write", value)) + } +} + +type FfiDestroyerSpError struct{} + +func (_ FfiDestroyerSpError) Destroy(value *SpError) { + switch variantValue := value.err.(type) { + case SpErrorNoCertificateChainFound: + variantValue.destroy() + case SpErrorC2paError: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerSpError.Destroy", value)) + } +} + type FfiConverterOptionalString struct{} var FfiConverterOptionalStringINSTANCE = FfiConverterOptionalString{} @@ -2120,10 +2268,10 @@ func iroh_streamplace_uniffiFreeGorutine(data C.uint64_t) { guard <- struct{}{} } -func GetManifest(data []byte) (string, *CertError) { - _uniffiRV, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { +func GetCert(data []byte) (string, *SpError) { + _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_func_get_manifest(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + inner: C.uniffi_iroh_streamplace_fn_func_get_cert(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), } }) if _uniffiErr != nil { @@ -2134,10 +2282,10 @@ func GetManifest(data []byte) (string, *CertError) { } } -func PrintCert(data []byte) (string, *CertError) { - _uniffiRV, _uniffiErr := rustCallWithError[CertError](FfiConverterCertError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { +func GetManifest(data []byte) (string, *SpError) { + _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_func_print_cert(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + inner: C.uniffi_iroh_streamplace_fn_func_get_manifest(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), } }) if _uniffiErr != nil { @@ -2147,3 +2295,17 @@ func PrintCert(data []byte) (string, *CertError) { return FfiConverterStringINSTANCE.Lift(_uniffiRV), _uniffiErr } } + +func Sign(manifest string, data []byte, certs []byte, gosigner GoSigner) ([]byte, *SpError) { + _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_func_sign(FfiConverterStringINSTANCE.Lower(manifest), FfiConverterBytesINSTANCE.Lower(data), FfiConverterBytesINSTANCE.Lower(certs), FfiConverterGoSignerINSTANCE.Lower(gosigner), _uniffiStatus), + } + }) + if _uniffiErr != nil { + var _uniffiDefaultValue []byte + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterBytesINSTANCE.Lift(_uniffiRV), _uniffiErr + } +} diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index a8bee0e8..d089312b 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -391,6 +391,20 @@ static void call_UniffiCallbackInterfaceDataHandlerMethod0( } +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 +typedef void (*UniffiCallbackInterfaceGoSignerMethod0)(uint64_t uniffi_handle, RustBuffer data, RustBuffer* uniffi_out_return, RustCallStatus* callStatus ); + +// Making function static works arround: +// https://github.com/golang/go/issues/11263 +static void call_UniffiCallbackInterfaceGoSignerMethod0( + UniffiCallbackInterfaceGoSignerMethod0 cb, uint64_t uniffi_handle, RustBuffer data, RustBuffer* uniffi_out_return, RustCallStatus* callStatus ) +{ + return cb(uniffi_handle, data, uniffi_out_return, callStatus ); +} + + #endif #ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER #define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER @@ -399,6 +413,14 @@ typedef struct UniffiVTableCallbackInterfaceDataHandler { UniffiCallbackInterfaceFree uniffiFree; } UniffiVTableCallbackInterfaceDataHandler; +#endif +#ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER +#define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER +typedef struct UniffiVTableCallbackInterfaceGoSigner { + UniffiCallbackInterfaceGoSignerMethod0 sign; + UniffiCallbackInterfaceFree uniffiFree; +} UniffiVTableCallbackInterfaceGoSigner; + #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLER #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLER @@ -441,6 +463,26 @@ uint64_t uniffi_iroh_streamplace_fn_constructor_endpoint_new(void uint64_t uniffi_iroh_streamplace_fn_method_endpoint_node_addr(void* ptr ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_GOSIGNER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_GOSIGNER +void* uniffi_iroh_streamplace_fn_clone_gosigner(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_GOSIGNER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_GOSIGNER +void uniffi_iroh_streamplace_fn_free_gosigner(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_GOSIGNER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_GOSIGNER +void uniffi_iroh_streamplace_fn_init_callback_vtable_gosigner(UniffiVTableCallbackInterfaceGoSigner* vtable +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_GOSIGNER_SIGN +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_GOSIGNER_SIGN +RustBuffer uniffi_iroh_streamplace_fn_method_gosigner_sign(void* ptr, RustBuffer data, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODEADDR #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODEADDR void* uniffi_iroh_streamplace_fn_clone_nodeaddr(void* ptr, RustCallStatus *out_status @@ -571,14 +613,19 @@ uint64_t uniffi_iroh_streamplace_fn_method_sender_node_addr(void* ptr uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key, RustBuffer data ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_CERT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_CERT +RustBuffer uniffi_iroh_streamplace_fn_func_get_cert(RustBuffer data, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST RustBuffer uniffi_iroh_streamplace_fn_func_get_manifest(RustBuffer data, RustCallStatus *out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_PRINT_CERT -RustBuffer uniffi_iroh_streamplace_fn_func_print_cert(RustBuffer data, RustCallStatus *out_status +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_SIGN +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_SIGN +RustBuffer uniffi_iroh_streamplace_fn_func_sign(RustBuffer manifest, RustBuffer data, RustBuffer certs, void* gosigner, RustCallStatus *out_status ); #endif #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC @@ -859,6 +906,12 @@ void ffi_iroh_streamplace_rust_future_free_void(uint64_t handle #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID #define UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUST_FUTURE_COMPLETE_VOID void ffi_iroh_streamplace_rust_future_complete_void(uint64_t handle, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_GET_CERT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_GET_CERT +uint16_t uniffi_iroh_streamplace_checksum_func_get_cert(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_GET_MANIFEST @@ -867,9 +920,9 @@ uint16_t uniffi_iroh_streamplace_checksum_func_get_manifest(void ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_PRINT_CERT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_PRINT_CERT -uint16_t uniffi_iroh_streamplace_checksum_func_print_cert(void +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_SIGN +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_SIGN +uint16_t uniffi_iroh_streamplace_checksum_func_sign(void ); #endif @@ -883,6 +936,12 @@ uint16_t uniffi_iroh_streamplace_checksum_method_datahandler_handle_data(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_ENDPOINT_NODE_ADDR uint16_t uniffi_iroh_streamplace_checksum_method_endpoint_node_addr(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_GOSIGNER_SIGN +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_GOSIGNER_SIGN +uint16_t uniffi_iroh_streamplace_checksum_method_gosigner_sign(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODEADDR_DIRECT_ADDRESSES @@ -1002,6 +1061,8 @@ uint32_t ffi_iroh_streamplace_uniffi_contract_version(void void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerMethod0(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerFree(uint64_t handle); + void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uint64_t uniffi_handle, RustBuffer data, RustBuffer* uniffi_out_return, RustCallStatus* callStatus ); + void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(uint64_t handle); void iroh_streamplace_uniffiFutureContinuationCallback(uint64_t, int8_t); void iroh_streamplace_uniffiFreeGorutine(uint64_t); diff --git a/pkg/media/media.go b/pkg/media/media.go index bc8795b3..a4240927 100644 --- a/pkg/media/media.go +++ b/pkg/media/media.go @@ -18,6 +18,7 @@ import ( "stream.place/streamplace/pkg/aqtime" "stream.place/streamplace/pkg/atproto" "stream.place/streamplace/pkg/bus" + c2patypes "stream.place/streamplace/pkg/c2patypes" "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/gstinit" "stream.place/streamplace/pkg/model" @@ -25,7 +26,6 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/replication" - "git.stream.place/streamplace/c2pa-go/pkg/c2pa/generated/manifeststore" "github.com/piprate/json-gold/ld" irohStreamplace "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" @@ -195,10 +195,10 @@ type SegmentMetadata struct { var ErrInvalidMetadata = errors.New("invalid segment metadata") -func ParseSegmentAssertions(ctx context.Context, mani *manifeststore.Manifest) (*SegmentMetadata, error) { +func ParseSegmentAssertions(ctx context.Context, mani *c2patypes.Manifest) (*SegmentMetadata, error) { _, span := otel.Tracer("signer").Start(ctx, "ParseSegmentAssertions") defer span.End() - var ass *manifeststore.ManifestAssertion + var ass *c2patypes.ManifestAssertion for _, a := range mani.Assertions { if a.Label == StreamplaceMetadata { ass = &a diff --git a/pkg/media/media_signer.go b/pkg/media/media_signer.go index 05f26a4e..c55ecd6f 100644 --- a/pkg/media/media_signer.go +++ b/pkg/media/media_signer.go @@ -1,25 +1,24 @@ package media import ( - "bytes" "context" "crypto" "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" "encoding/json" "fmt" "io" - "path/filepath" "time" - "git.stream.place/streamplace/c2pa-go/pkg/c2pa" "go.opentelemetry.io/otel" - "stream.place/streamplace/pkg/aqio" "stream.place/streamplace/pkg/aqtime" "stream.place/streamplace/pkg/atproto" + c2patypes "stream.place/streamplace/pkg/c2patypes" "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/crypto/aqpub" "stream.place/streamplace/pkg/crypto/signers" - "stream.place/streamplace/pkg/log" + "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" "stream.place/streamplace/pkg/spmetrics" ) @@ -28,6 +27,7 @@ type MediaSigner interface { Pub() aqpub.Pub Streamer() string DID() string + Sign(data []byte) ([]byte, *iroh_streamplace.SpError) } type MediaSignerLocal struct { @@ -39,40 +39,18 @@ type MediaSignerLocal struct { did string } -func prepareCert(ctx context.Context, cli *config.CLI, signer crypto.Signer) ([]byte, string, error) { - pub, err := aqpub.FromPublicKey(signer.Public().(*ecdsa.PublicKey)) - if err != nil { - return nil, "", err - } - fSlice := []string{pub.String(), CertFile} - exists, err := cli.DataFileExists(fSlice) +func prepareCert(ctx context.Context, cli *config.CLI, signer crypto.Signer) ([]byte, error) { + + cert, err := signers.GenerateES256KCert(signer) if err != nil { - return nil, "", err - } - if !exists { - cert, err := signers.GenerateES256KCert(signer) - if err != nil { - return nil, "", err - } - r := bytes.NewReader(cert) - err = cli.DataFileWrite(fSlice, r, false) - if err != nil { - return nil, "", err - } - log.Log(ctx, "wrote new media signing certificate", "file", filepath.Join(pub.String(), CertFile)) - } - buf := bytes.Buffer{} - if err := cli.DataFileRead(fSlice, &buf); err != nil { - return nil, "", err + return nil, err } - fPath := cli.DataFilePath(fSlice) - cert := buf.Bytes() - return cert, fPath, nil + return cert, nil } func MakeMediaSigner(ctx context.Context, cli *config.CLI, streamer string, signer crypto.Signer) (MediaSigner, error) { - cert, _, err := prepareCert(ctx, cli, signer) + cert, err := prepareCert(ctx, cli, signer) if err != nil { return nil, err } @@ -133,43 +111,26 @@ func (ms *MediaSignerLocal) SignMP4(ctx context.Context, input io.ReadSeeker, st if err != nil { return nil, fmt.Errorf("failed to marshal manifest: %w", err) } - var manifest c2pa.ManifestDefinition + var manifest c2patypes.ManifestDefinition err = json.Unmarshal(manifestBs, &manifest) if err != nil { return nil, fmt.Errorf("failed to unmarshal manifest: %w", err) } span.End() - ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_GetSigningAlgorithm") - alg, err := c2pa.GetSigningAlgorithm(string(c2pa.ES256K)) - if err != nil { - return nil, fmt.Errorf("failed to get signing algorithm: %w", err) - } - span.End() - - ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_NewBuilder") - b, err := c2pa.NewBuilder(&manifest, &c2pa.BuilderParams{ - Cert: ms.Cert, - Signer: ms.Signer, - Algorithm: alg, - TAURL: ms.TAURL, - }) + bs, err := io.ReadAll(input) if err != nil { - return nil, fmt.Errorf("failed to create C2PA builder: %w", err) + return nil, fmt.Errorf("failed to read input: %w", err) } - span.End() - ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_Sign") - output := &aqio.ReadWriteSeeker{} - err = b.Sign(input, output, "video/mp4") - if err != nil { - return nil, fmt.Errorf("failed to sign MP4: %w", err) + bs, rustErr := iroh_streamplace.Sign(string(manifestBs), bs, ms.Cert, ms) + if rustErr.AsError() != nil { + return nil, rustErr.AsError() } span.End() ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_OutputBytes") defer ctx.Done() - bs, err := output.Bytes() if err != nil { return nil, fmt.Errorf("failed to get output bytes: %w", err) } @@ -178,6 +139,15 @@ func (ms *MediaSignerLocal) SignMP4(ctx context.Context, input io.ReadSeeker, st return bs, nil } +func (ms *MediaSignerLocal) Sign(data []byte) ([]byte, *iroh_streamplace.SpError) { + digest := sha256.Sum256(data) + sig, err := ms.Signer.Sign(rand.Reader, digest[:], nil) + if err != nil { + return nil, iroh_streamplace.NewSpErrorNoCertificateChainFound() + } + return sig, nil +} + func (ms *MediaSignerLocal) Pub() aqpub.Pub { return ms.AQPub } diff --git a/pkg/media/media_signer_ext.go b/pkg/media/media_signer_ext.go index 66fd7aa6..64b9d25b 100644 --- a/pkg/media/media_signer_ext.go +++ b/pkg/media/media_signer_ext.go @@ -1,128 +1,108 @@ package media -import ( - "bytes" - "context" - "crypto" - "crypto/ecdsa" - "fmt" - "io" - "os" - "os/exec" - "time" - - "github.com/decred/dcrd/dcrec/secp256k1" - "github.com/mr-tron/base58" - "go.opentelemetry.io/otel" - "stream.place/streamplace/pkg/atproto" - "stream.place/streamplace/pkg/config" - "stream.place/streamplace/pkg/crypto/aqpub" - "stream.place/streamplace/pkg/spmetrics" -) - -type MediaSignerExt struct { - cli *config.CLI - signer crypto.Signer - pub aqpub.Pub - certPath string - streamer string - keyBs []byte - taURL string - did string -} - -func MakeMediaSignerExt(ctx context.Context, cli *config.CLI, streamer string, keyBs []byte) (MediaSigner, error) { - key, _ := secp256k1.PrivKeyFromBytes(keyBs) - if key == nil { - return nil, fmt.Errorf("invalid authorization key (not valid secp256k1)") - } - var signer crypto.Signer = key.ToECDSA() - _, certPath, err := prepareCert(ctx, cli, signer) - if err != nil { - return nil, err - } - pub, err := aqpub.FromPublicKey(signer.Public().(*ecdsa.PublicKey)) - if err != nil { - return nil, err - } - did, err := atproto.ParsePubKey(signer.Public().(*ecdsa.PublicKey)) - if err != nil { - return nil, err - } - return &MediaSignerExt{ - // cli: cli, - signer: signer, - certPath: certPath, - streamer: streamer, - pub: pub, - keyBs: keyBs, - taURL: cli.TAURL, - did: did.DIDKey(), - }, nil -} - -func (ms *MediaSignerExt) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { - startTime := time.Now() - _, span := otel.Tracer("signer").Start(ctx, "SignMP4_Ext") - defer span.End() - // Get the path to the current executable - execPath, err := os.Executable() - if err != nil { - return nil, fmt.Errorf("failed to get executable path: %w", err) - } - - enc := base58.Encode(ms.keyBs) - - // Prepare command - cmd := exec.Command(execPath, "sign", - "--key", enc, - "--cert", ms.certPath, - "--ta-url", ms.taURL, - "--streamer", ms.streamer, - "--start-time", fmt.Sprintf("%d", start)) - - // overwrite so that our subprocesses don't do their own leak checking - cmd.Env = append(os.Environ(), "LD_PRELOAD=") - - // Set up pipes for stdin and stdout - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, fmt.Errorf("failed to create stdin pipe: %w", err) - } - - stdout := &bytes.Buffer{} - cmd.Stdout = stdout - stderr := &bytes.Buffer{} - cmd.Stderr = stderr - - // Start the command - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start command: %w", err) - } - - // Copy input to stdin - _, err = io.Copy(stdin, input) - if err != nil { - return nil, fmt.Errorf("failed to write to stdin: %w stderr=%s", err, stderr.String()) - } - stdin.Close() - - // Wait for the command to complete - if err := cmd.Wait(); err != nil { - return nil, fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String()) - } - spmetrics.SigningDuration.WithLabelValues(ms.streamer).Observe(float64(time.Since(startTime).Milliseconds())) - return stdout.Bytes(), nil -} - -func (ms *MediaSignerExt) Pub() aqpub.Pub { - return ms.pub -} - -func (ms *MediaSignerExt) Streamer() string { - return ms.streamer -} - -func (ms *MediaSignerExt) DID() string { - return ms.did -} +// type MediaSignerExt struct { +// cli *config.CLI +// signer crypto.Signer +// pub aqpub.Pub +// certPath string +// streamer string +// keyBs []byte +// taURL string +// did string +// } + +// func MakeMediaSignerExt(ctx context.Context, cli *config.CLI, streamer string, keyBs []byte) (MediaSigner, error) { +// key, _ := secp256k1.PrivKeyFromBytes(keyBs) +// if key == nil { +// return nil, fmt.Errorf("invalid authorization key (not valid secp256k1)") +// } +// var signer crypto.Signer = key.ToECDSA() +// _, certPath, err := prepareCert(ctx, cli, signer) +// if err != nil { +// return nil, err +// } +// pub, err := aqpub.FromPublicKey(signer.Public().(*ecdsa.PublicKey)) +// if err != nil { +// return nil, err +// } +// did, err := atproto.ParsePubKey(signer.Public().(*ecdsa.PublicKey)) +// if err != nil { +// return nil, err +// } +// return &MediaSignerExt{ +// // cli: cli, +// signer: signer, +// certPath: certPath, +// streamer: streamer, +// pub: pub, +// keyBs: keyBs, +// taURL: cli.TAURL, +// did: did.DIDKey(), +// }, nil +// } + +// func (ms *MediaSignerExt) SignMP4(ctx context.Context, input io.ReadSeeker, start int64) ([]byte, error) { +// startTime := time.Now() +// _, span := otel.Tracer("signer").Start(ctx, "SignMP4_Ext") +// defer span.End() +// // Get the path to the current executable +// execPath, err := os.Executable() +// if err != nil { +// return nil, fmt.Errorf("failed to get executable path: %w", err) +// } + +// enc := base58.Encode(ms.keyBs) + +// // Prepare command +// cmd := exec.Command(execPath, "sign", +// "--key", enc, +// "--cert", ms.certPath, +// "--ta-url", ms.taURL, +// "--streamer", ms.streamer, +// "--start-time", fmt.Sprintf("%d", start)) + +// // overwrite so that our subprocesses don't do their own leak checking +// cmd.Env = append(os.Environ(), "LD_PRELOAD=") + +// // Set up pipes for stdin and stdout +// stdin, err := cmd.StdinPipe() +// if err != nil { +// return nil, fmt.Errorf("failed to create stdin pipe: %w", err) +// } + +// stdout := &bytes.Buffer{} +// cmd.Stdout = stdout +// stderr := &bytes.Buffer{} +// cmd.Stderr = stderr + +// // Start the command +// if err := cmd.Start(); err != nil { +// return nil, fmt.Errorf("failed to start command: %w", err) +// } + +// // Copy input to stdin +// _, err = io.Copy(stdin, input) +// if err != nil { +// return nil, fmt.Errorf("failed to write to stdin: %w stderr=%s", err, stderr.String()) +// } +// stdin.Close() + +// // Wait for the command to complete +// if err := cmd.Wait(); err != nil { +// return nil, fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String()) +// } +// spmetrics.SigningDuration.WithLabelValues(ms.streamer).Observe(float64(time.Since(startTime).Milliseconds())) +// return stdout.Bytes(), nil +// } + +// func (ms *MediaSignerExt) Pub() aqpub.Pub { +// return ms.pub +// } + +// func (ms *MediaSignerExt) Streamer() string { +// return ms.streamer +// } + +// func (ms *MediaSignerExt) DID() string { +// return ms.did +// } diff --git a/pkg/media/validate.go b/pkg/media/validate.go index e6f0e2b9..d832d731 100644 --- a/pkg/media/validate.go +++ b/pkg/media/validate.go @@ -3,18 +3,19 @@ package media import ( "bytes" "context" + "encoding/json" "fmt" "io" "strings" "go.opentelemetry.io/otel" "stream.place/streamplace/pkg/aqtime" + c2patypes "stream.place/streamplace/pkg/c2patypes" "stream.place/streamplace/pkg/constants" "stream.place/streamplace/pkg/crypto/signers" + "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/model" - - "git.stream.place/streamplace/c2pa-go/pkg/c2pa" ) func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error { @@ -24,18 +25,24 @@ func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error if err != nil { return err } - r := bytes.NewReader(buf) - reader, err := c2pa.FromStream(r, "video/mp4") + maniStr, rustErr := iroh_streamplace.GetManifest(buf) + if rustErr.AsError() != nil { + return rustErr.AsError() + } + var mani c2patypes.Manifest + err = json.Unmarshal([]byte(maniStr), &mani) if err != nil { return err } - mani := reader.GetActiveManifest() - certs := reader.GetProvenanceCertChain() - pub, err := signers.ParseES256KCert([]byte(certs)) + certStr, rustErr := iroh_streamplace.GetCert(buf) + if rustErr.AsError() != nil { + return rustErr.AsError() + } + pub, err := signers.ParseES256KCert([]byte(certStr)) if err != nil { return err } - meta, err := ParseSegmentAssertions(ctx, mani) + meta, err := ParseSegmentAssertions(ctx, &mani) if err != nil { return err } @@ -75,7 +82,7 @@ func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error } defer fd.Close() go mm.replicator.NewSegment(ctx, buf) - r = bytes.NewReader(buf) + r := bytes.NewReader(buf) if _, err := io.Copy(fd, r); err != nil { return err } diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs index 3207d743..be954fa9 100644 --- a/rust/iroh-streamplace/src/c2pa.rs +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -1,9 +1,13 @@ +use c2pa::Builder; +use c2pa::CallbackSigner; use c2pa::Reader; +use c2pa::settings::Settings; use std::io::Cursor; +use std::sync::Arc; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] -pub enum CertError { +pub enum SPError { #[error("No certificate chain found")] NoCertificateChainFound, #[error("C2PA error: {0}")] @@ -11,27 +15,123 @@ pub enum CertError { } #[uniffi::export] -pub fn print_cert(data: Vec) -> Result { +pub fn get_cert(data: Vec) -> Result { let reader = Reader::from_stream("video/mp4", Cursor::new(data)) - .map_err(|e| CertError::C2paError(e.to_string()))?; - // todo: add cawg certs here?? + .map_err(|e| SPError::C2paError(e.to_string()))?; if let Some(manifest) = reader.active_manifest() { if let Some(si) = manifest.signature_info() { - println!("{}", si.cert_chain()); - // todo: add ocsp validation info return Ok(si.cert_chain().to_string()); } } - Err(CertError::NoCertificateChainFound) + Err(SPError::NoCertificateChainFound) } #[uniffi::export] -pub fn get_manifest(data: Vec) -> Result { +pub fn get_manifest(data: Vec) -> Result { let reader = Reader::from_stream("video/mp4", Cursor::new(data)) - .map_err(|e| CertError::C2paError(e.to_string()))?; - // todo: add cawg certs here?? + .map_err(|e| SPError::C2paError(e.to_string()))?; if let Some(manifest) = reader.active_manifest() { return Ok(manifest.to_string()); } - Err(CertError::NoCertificateChainFound) + Err(SPError::NoCertificateChainFound) +} + +#[uniffi::export(with_foreign)] +pub trait GoSigner: Send + Sync { + fn sign(&self, data: Vec) -> Result, SPError>; +} + +// #[derive(uniffi::Object)] +// struct Authenticator { +// gosigner: Arc, +// } + +// impl Authenticator { +// pub fn new(gosigner: Arc) -> Self { +// Self { gosigner } +// } + +// pub fn login(&self) { +// let username = self.gosigner.get("username".into()); +// let password = self.gosigner.get("password".into()); +// } +// } + +const TOML_SETTINGS: &str = r#" +version_major = 1 +version_minor = 0 + +[trust] + +[core] +debug = true +hash_alg = "sha256" +salt_jumbf_boxes = true +prefer_box_hash = false +merkle_tree_max_proofs = 5 +compress_manifests = true + +[verify] +verify_after_reading = false +verify_after_sign = false +verify_trust = false +verify_timestamp_trust = false +ocsp_fetch = false +remote_manifest_fetch = false +check_ingredient_trust = false +skip_ingredient_conflict_resolution = false +strict_v1_validation = false + +[builder.thumbnail] +enabled = false +ignore_errors = true +long_edge = 1024 +prefer_smallest_format = true +quality = "medium" + +[builder.actions] +all_actions_included = false + +[builder.actions.auto_created_action] +enabled = true +source_type = "http://c2pa.org/digitalsourcetype/empty" + +[builder.actions.auto_opened_action] +enabled = true + +[builder.actions.auto_placed_action] +enabled = true +"#; + +#[uniffi::export] +pub fn sign( + manifest: String, + data: Vec, + certs: Vec, + gosigner: Arc, +) -> Result, SPError> { + Settings::from_toml(TOML_SETTINGS).map_err(|e| SPError::C2paError(e.to_string()))?; + let callback_signer = CallbackSigner::new( + move |_context: *const (), data: &[u8]| { + gosigner + .sign(data.to_vec()) + .map_err(|e| c2pa::Error::BadParam(e.to_string())) + }, + c2pa::SigningAlg::Es256K, + certs, + ); + let mut builder = + Builder::from_json(&manifest).map_err(|e| SPError::C2paError(e.to_string()))?; + let mut output = Vec::new(); + let mut input_cursor = Cursor::new(data); + let mut output_cursor = Cursor::new(&mut output); + builder + .sign( + &callback_signer, + "video/mp4", + &mut input_cursor, + &mut output_cursor, + ) + .map_err(|e| SPError::C2paError(e.to_string()))?; + Ok(output) } -- 2.51.2 From 3e603cf4ff28949add7b1d23226fb143d34d0c27 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 23 Sep 2025 17:40:21 -0700 Subject: [PATCH 037/525] c2pa: remove c2pa-go, we use c2pa-rs directly now --- Makefile | 6 ------ go.mod | 3 +-- go.sum | 2 -- meson.build | 19 ++++--------------- pkg/crypto/signers/signers.go | 5 +++-- pkg/media/media_test.go | 12 +++++------- subprojects/c2pa_go.wrap | 4 ---- 7 files changed, 13 insertions(+), 38 deletions(-) delete mode 100644 subprojects/c2pa_go.wrap diff --git a/Makefile b/Makefile index 35505d67..82064c5e 100644 --- a/Makefile +++ b/Makefile @@ -659,12 +659,6 @@ link-mist: rm -rf subprojects/mistserver ln -s $$(realpath ../mistserver) ./subprojects/mistserver -# link your local version of c2pa-go for dev -.PHONY: link-c2pa-go -link-c2pa-go: - rm -rf subprojects/c2pa_go - ln -s $$(realpath ../c2pa-go) ./subprojects/c2pa_go - # link your local version of gstreamer .PHONY: link-gstreamer link-gstreamer: diff --git a/go.mod b/go.mod index 82c84289..16bbd390 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ replace github.com/bluesky-social/indigo => github.com/streamplace/indigo v0.0.0 require ( firebase.google.com/go/v4 v4.14.1 - git.stream.place/streamplace/c2pa-go v0.7.0 github.com/99designs/gqlgen v0.17.64 github.com/NYTimes/gziphandler v1.1.1 github.com/ThalesGroup/crypto11 v0.0.0-00010101000000-000000000000 @@ -66,6 +65,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 go.opentelemetry.io/otel/sdk v1.36.0 go.opentelemetry.io/otel/trace v1.36.0 + go.uber.org/goleak v1.3.0 golang.org/x/image v0.30.0 golang.org/x/net v0.43.0 golang.org/x/sync v0.16.0 @@ -500,7 +500,6 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.41.0 // indirect diff --git a/go.sum b/go.sum index 68dd8ea4..0a1ba897 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,6 @@ git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= -git.stream.place/streamplace/c2pa-go v0.7.0 h1:cRbOxgAFgM42XDmvK39ImYP/LrJEekYZ+GoxMGIcakk= -git.stream.place/streamplace/c2pa-go v0.7.0/go.mod h1:ymtm/Gk3K8Q67SuBoZUqmA0prdymNwAI9BjLj6plhBM= github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= github.com/99designs/gqlgen v0.17.64 h1:BzpqO5ofQXyy2XOa93Q6fP1BHLRjTOeU35ovTEsbYlw= diff --git a/meson.build b/meson.build index 611e01b8..adc7b9f4 100644 --- a/meson.build +++ b/meson.build @@ -41,17 +41,6 @@ if meson.is_cross_build() } endif -c2pa_go_opts = {} -triple = meson.get_external_property('RUST_TRIPLE', '') -if triple != '' - c2pa_go_opts += {'RUST_TRIPLE': triple} -endif -c2pa_go_proj = subproject( - 'c2pa_go', - default_options: c2pa_go_opts, -) -c2pa_go_dep = c2pa_go_proj.get_variable('c2pa_go_dep') - streamplace_deps = [] # if host_machine.system() == 'linux' # mistserver_proj = subproject( @@ -246,7 +235,7 @@ endif pkg = import('pkgconfig') pkg.generate( name: 'streamplacedeps', - libraries: [streamplace_deps, c2pa_go_dep, iroh_streamplace_dep], + libraries: [streamplace_deps, iroh_streamplace_dep], description: 'all streamplace dependencies bundled for easy inclusion', ) @@ -257,7 +246,7 @@ executable( dependencies: [ streamplace_deps, ], - link_with: [c2pa_go_dep, iroh_streamplace_dep], + link_with: [iroh_streamplace_dep], install_tag: 'streamplacedeps', ) @@ -318,7 +307,7 @@ if get_option('default_library') == 'static' dependencies: [ streamplace_deps, ], - link_with: [c2pa_go_dep, iroh_streamplace_dep], + link_with: [iroh_streamplace_dep], ) streamplace = custom_target( @@ -343,7 +332,7 @@ if get_option('default_library') == 'static' dependencies: [ streamplace_deps, ], - link_with: [c2pa_go_dep, iroh_streamplace_dep], + link_with: [iroh_streamplace_dep], ) endif diff --git a/pkg/crypto/signers/signers.go b/pkg/crypto/signers/signers.go index 7722324e..653438c1 100644 --- a/pkg/crypto/signers/signers.go +++ b/pkg/crypto/signers/signers.go @@ -14,12 +14,13 @@ import ( "math/big" "time" - "git.stream.place/streamplace/c2pa-go/pkg/c2pa" atcrypto "github.com/bluesky-social/indigo/atproto/crypto" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" ) +var OID_SECP256K1 asn1.ObjectIdentifier = []int{1, 3, 132, 0, 10} + // uses Go code to generate a es256p cert, then rewrites and resigns it into an es256k cert func GenerateES256KCert(signer gocrypto.Signer) ([]byte, error) { keyUsage := x509.KeyUsageDigitalSignature @@ -78,7 +79,7 @@ func GenerateES256KCert(signer gocrypto.Signer) ([]byte, error) { old := p256cert.TBSCertificate var paramBytes []byte - paramBytes, err = asn1.Marshal(c2pa.OID_SECP256K1) + paramBytes, err = asn1.Marshal(OID_SECP256K1) if err != nil { return nil, fmt.Errorf("failed to marshal OID_SECP256K1: %w", err) diff --git a/pkg/media/media_test.go b/pkg/media/media_test.go index 1fab4e9c..9883e54f 100644 --- a/pkg/media/media_test.go +++ b/pkg/media/media_test.go @@ -7,13 +7,11 @@ import ( "runtime" "testing" - "git.stream.place/streamplace/c2pa-go/pkg/c2pa" "github.com/stretchr/testify/require" "stream.place/streamplace/pkg/atproto" "stream.place/streamplace/pkg/bus" "stream.place/streamplace/pkg/config" ct "stream.place/streamplace/pkg/config/configtesting" - "stream.place/streamplace/pkg/crypto/signers/eip712/eip712test" "stream.place/streamplace/pkg/model" "stream.place/streamplace/pkg/replication/boring" ) @@ -27,7 +25,7 @@ func getFixture(name string) string { func getStaticTestMediaManager(t *testing.T) (*MediaManager, MediaSigner) { mod, err := model.MakeDB(":memory:") require.NoError(t, err) - signer, err := c2pa.MakeStaticSigner(eip712test.KeyBytes) + // signer, err := c2pa.MakeStaticSigner(eip712test.KeyBytes) require.NoError(t, err) if err != nil { panic(err) @@ -42,11 +40,11 @@ func getStaticTestMediaManager(t *testing.T) (*MediaManager, MediaSigner) { StatefulDB: nil, // Test doesn't need StatefulDB for now Bus: bus.NewBus(), } - mm, err := MakeMediaManager(context.Background(), cli, signer, &boring.BoringReplicator{}, mod, bus.NewBus(), atsync) + mm, err := MakeMediaManager(context.Background(), cli, nil, &boring.BoringReplicator{}, mod, bus.NewBus(), atsync) require.NoError(t, err) - ms, err := MakeMediaSigner(context.Background(), cli, "test-person", signer) - require.NoError(t, err) - return mm, ms + // ms, err := MakeMediaSigner(context.Background(), cli, "test-person", signer) + // require.NoError(t, err) + return mm, nil } // func mp4(t *testing.T) []byte { diff --git a/subprojects/c2pa_go.wrap b/subprojects/c2pa_go.wrap deleted file mode 100644 index 180a9380..00000000 --- a/subprojects/c2pa_go.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -url = https://github.com/streamplace/c2pa-go.git -revision = v0.7.2 -depth = 1 -- 2.51.2 From f7cd4219b242494b339045b616dcc1c209d9fa3f Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Sep 2025 20:42:08 -0500 Subject: [PATCH 038/525] fix player layout issues, --- js/app/components/mobile/chat.tsx | 5 +++-- js/app/components/mobile/player.tsx | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/js/app/components/mobile/chat.tsx b/js/app/components/mobile/chat.tsx index 81da17a1..520ec8fb 100644 --- a/js/app/components/mobile/chat.tsx +++ b/js/app/components/mobile/chat.tsx @@ -85,6 +85,7 @@ export function MobileChatPanel({ isPlayerRatioGreater }) { ? layout.position.relative : layout.position.absolute, bottom[0], + px[2], { width: "100%", maxWidth: "100%" }, ]} > @@ -119,7 +120,7 @@ function ChatPanel() { - + {agent?.did ? ( void; }, ) { - const [showChat, setShowChat] = useState(!props.ingest); + const [showChat, setShowChat] = useState(true); const { shouldShowChatSidePanel, chatPanelWidth, safeAreaInsets } = useResponsiveLayout(); const chatVisible = shouldShowChatSidePanel && showChat; @@ -196,14 +196,20 @@ export function PlayerInner( return ( Date: Tue, 23 Sep 2025 20:46:12 -0500 Subject: [PATCH 039/525] fix overlay and other gesture handling --- js/app/components/mobile/ui.tsx | 361 ++++++++++++++++---------------- 1 file changed, 186 insertions(+), 175 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 04305a74..d4863760 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -31,6 +31,7 @@ import { useEffect, useRef, useState } from "react"; import { Image, Platform, Pressable } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { + runOnJS, useAnimatedStyle, useSharedValue, withTiming, @@ -120,9 +121,16 @@ export function MobileUi({ }; }, []); + const showUI = () => { + "worklet"; + fadeOpacity.value = withTiming(1, { duration: 200 }); + }; + const onPlayerHover = () => { "worklet"; - resetFadeTimer(); + showUI(); + // Schedule timer reset on JS thread + runOnJS(resetFadeTimer)(); }; const animatedFadeStyle = useAnimatedStyle(() => ({ @@ -137,209 +145,212 @@ export function MobileUi({ const combined = Gesture.Race(hover, pan); return ( - - <> - + + - {/* Main UI Overlay */} - - {/* Top Left - Back Button and Profile */} + {/* Main UI Overlay */} - - { - navigation.canGoBack() - ? navigation.goBack() - : navigation.navigate("Home", { screen: "StreamList" }); - }} - > - - - - {profile?.handle} - - - - {/* Right Controls Column */} - - {shouldShowFloatingMetrics && ( - - + + { + navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate("Home", { screen: "StreamList" }); + }} + > + + + - - + /> + {profile?.handle} - )} - - - + - {shouldShowFloatingMetrics && isLive && ( + {/* Right Controls Column */} - + + + + + )} + + - )} - - {isSelfAndNotLive && ( - - )} - - { - if (!recordSubmitted && title != "") { - setShowLoading(true); - } - setShowCountdown(false); - }} - /> - - - + {shouldShowFloatingMetrics && isLive && ( + + + + )} + + {isSelfAndNotLive && ( + + )} - {showChat === undefined && ( - - )} - {muted && ( - - { - if (muteWasForced) { - setMuted(false); - setMuteWasForced(false); - } else { - setMuted(false); + { + if (!recordSubmitted && title != "") { + setShowLoading(true); } + setShowCountdown(false); }} + /> + + + + + {muted && ( + - { + if (muteWasForced) { + setMuted(false); + setMuteWasForced(false); + } else { + setMuted(false); + } + }} style={[ { - padding: 4, - backgroundColor: "rgba(50, 30, 30, 0.4)", - borderRadius: 999, - borderWidth: 2, - borderColor: "rgba(255, 120, 120, 0.2)", + flexDirection: "row", + alignItems: "center", + gap: 8, }, ]} > - - - - Tap to unmute - - - - )} - - - + + + + + Tap to unmute + + + + )} + + + + {showChat === undefined && ( + + )} + ); } -- 2.51.2 From f0561363e752c3da9404b7022f73049f1e251e44 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Sep 2025 21:05:33 -0500 Subject: [PATCH 040/525] fade out ui on mobile on landscape --- js/app/components/mobile/ui.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index d4863760..f318e567 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -135,15 +135,16 @@ export function MobileUi({ const animatedFadeStyle = useAnimatedStyle(() => ({ opacity: - shouldShowFloatingMetrics || shouldShowChatSidePanel + shouldShowFloatingMetrics || showChat === undefined ? 1 : fadeOpacity.value, })); const hover = Gesture.Hover().onChange(onPlayerHover); const pan = Gesture.Pan().onChange(onPlayerHover); + const tap = Gesture.Tap().onEnd(onPlayerHover); - const combined = Gesture.Race(hover, pan); + const combined = Gesture.Race(hover, pan, tap); return ( <> @@ -412,9 +413,9 @@ function RightControlsPanel({ ? zero.layout.flex.column : zero.layout.flex.row, zero.layout.flex.center, - zero.gap.all[3], + zero.gap.all[4], zero.py[2], - zero.px[1], + showChat === undefined ? zero.px[2] : zero.px[3], zero.layout.position.relative, ]} > -- 2.51.2 From cba5725b261e9666eb5d54d600758399e7a47211 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Sep 2025 21:06:44 -0500 Subject: [PATCH 041/525] remove landscape height override in player component --- js/app/components/mobile/player.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 3d33ac3e..c8fc732d 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -223,8 +223,7 @@ export function PlayerInner( } : { flex: 1, - // i hate this but it works - maxHeight: isLandscape && !props.showChat ? "90%" : "auto", + maxHeight: "auto", }, { paddingTop: -- 2.51.2 From e49629faaa0249cb159de4c49538d52823528595 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Sep 2025 21:24:23 -0500 Subject: [PATCH 042/525] hide/show status bar on android so it doesn't get in the way of andoid --- js/app/components/mobile/player.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index c8fc732d..e23b8967 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -19,7 +19,7 @@ import { useLiveUser } from "hooks/useLiveUser"; import { useSidebarControl } from "hooks/useSidebarControl"; import { ArrowLeft, ArrowRight } from "lucide-react-native"; import { ComponentRef, useEffect, useRef, useState } from "react"; -import { Animated, ScrollView } from "react-native"; +import { Animated, ScrollView, StatusBar } from "react-native"; import { useAppSelector } from "store/hooks"; import { BottomMetadata } from "./bottom-metadata"; import { DesktopChatPanel } from "./chat"; @@ -55,6 +55,12 @@ export function Player( const navigation = useNavigation(); + useEffect(() => { + return () => { + StatusBar.setHidden(false, "slide"); + }; + }, []); + if (isStreamingElsewhere) { return ( @@ -116,6 +122,7 @@ export function Player( return ( + -- 2.51.2 From f8af675d9e06b3e4d183cf489302675489fa333b Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Sat, 27 Sep 2025 07:14:21 +0100 Subject: [PATCH 059/525] Remove the slew of padding in this box Change the server and stream key display to readonly inputs Add an explict copy to clipboard button Change streamkey field to password and show it/select all on click --- .../components/live-dashboard/stream-key.tsx | 137 ++++++++++++++++-- 1 file changed, 124 insertions(+), 13 deletions(-) diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index 6c36f54e..b7d7e5a0 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -2,17 +2,18 @@ import { Body, Button, Code, Row, View } from "@streamplace/components"; import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; import { - clearStreamKeyRecord, - createStreamKeyRecord, - selectIsReady, - selectUserProfile, + clearStreamKeyRecord, + createStreamKeyRecord, + selectIsReady, + selectUserProfile, } from "features/bluesky/blueskySlice"; import { useEffect, useState } from "react"; +import { TextInput } from "react-native"; import { useAppDispatch, useAppSelector } from "store/hooks"; const FormRow = ({ children }: { children: React.ReactNode }) => { return ( - + {children} ); @@ -66,18 +67,16 @@ export function StreamKeyScreen() { WHIP - {protocol === "whip" && } {protocol === "rtmp" && } - - Output mode: Advanced + Output mode: Advanced +
Keyframe Interval: 1s - - +
x264 Options: bframes=0
@@ -99,7 +98,20 @@ export function WHIPDescription({ url }: { url: string }) { - {url} + @@ -127,7 +139,20 @@ export function RTMPDescription({ url }: { url: string }) { - {rtmpUrl} + @@ -145,6 +170,8 @@ export default StreamKeyScreen; export function StreamKey() { const dispatch = useAppDispatch(); const [generating, setGenerating] = useState(false); + const [hidekey, setHidekey] = useState(true); + const [didcopy, setDidcopy] = useState(false); const newKey = useAppSelector((state) => state.bluesky.newKey); useEffect(() => { @@ -168,12 +195,95 @@ export function StreamKey() { }; }, [newKey, dispatch]); + const handleCopy = async () => { + if (!newKey) { + return; + } + + try { + await navigator.clipboard.writeText(newKey.privateKey); + // TODO: Replace with custom toast implementation + console.log("Bearer token copied to clipboard"); + setDidcopy(true); + } catch (e) { + // not allowed. oh well. + console.log("Could not copy to clipboard"); + } + }; + if (generating) { return ; } if (newKey) { - return {newKey.privateKey}; + + return ( + + { + setHidekey(false); + }} + onBlur={() => { + setHidekey(true); + }} + selectTextOnFocus={true} + /> + + + ); } return ( @@ -181,6 +291,7 @@ export function StreamKey() { onPress={async () => { try { setGenerating(true); + setDidcopy(false); await dispatch(createStreamKeyRecord({ store: false })); } catch (e) { console.error("failed to generate stream key", e); -- 2.51.2 From 724b3c98542b7b21ccfa3186b6a7815ef4cc1c8a Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Sat, 27 Sep 2025 07:50:47 +0100 Subject: [PATCH 060/525] Throw it into a ScrollView on the box so it scrolls on smoll height browsers and tweak padding --- .../components/live-dashboard/stream-key.tsx | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index b7d7e5a0..0d3274ab 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -8,7 +8,7 @@ import { selectUserProfile, } from "features/bluesky/blueskySlice"; import { useEffect, useState } from "react"; -import { TextInput } from "react-native"; +import { ScrollView, TextInput } from "react-native"; import { useAppDispatch, useAppSelector } from "store/hooks"; const FormRow = ({ children }: { children: React.ReactNode }) => { @@ -51,38 +51,40 @@ export function StreamKeyScreen() { const url = useAppSelector((state) => state.streamplace.url); return ( - - - - - - - {protocol === "whip" && } - {protocol === "rtmp" && } - - - - - Output mode: Advanced -
- Keyframe Interval: 1s -
- x264 Options: bframes=0 - -
-
+ + + + + + + + {protocol === "whip" && } + {protocol === "rtmp" && } + + + + + Output mode: Advanced +
+ Keyframe Interval: 1s +
+ x264 Options: bframes=0 + +
+
+
-
+ ); } @@ -216,13 +218,8 @@ export function StreamKey() { } if (newKey) { - return ( - + Date: Sat, 27 Sep 2025 07:58:23 +0100 Subject: [PATCH 061/525] Drop bootstrap icons for lucide icons --- .../components/live-dashboard/stream-key.tsx | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index 0d3274ab..2c18e85f 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -1,4 +1,4 @@ -import { Body, Button, Code, Row, View } from "@streamplace/components"; +import { Body, Button, Code, Row, View, useTheme } from "@streamplace/components"; import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; import { @@ -7,6 +7,7 @@ import { selectIsReady, selectUserProfile, } from "features/bluesky/blueskySlice"; +import { Clipboard, ClipboardCheck } from "lucide-react-native"; import { useEffect, useState } from "react"; import { ScrollView, TextInput } from "react-native"; import { useAppDispatch, useAppSelector } from "store/hooks"; @@ -170,12 +171,16 @@ export function RTMPDescription({ url }: { url: string }) { export default StreamKeyScreen; export function StreamKey() { + const theme = useTheme(); + const dispatch = useAppDispatch(); const [generating, setGenerating] = useState(false); const [hidekey, setHidekey] = useState(true); const [didcopy, setDidcopy] = useState(false); const newKey = useAppSelector((state) => state.bluesky.newKey); + let foregroundColor = theme.theme.colors.text || "#fff"; + useEffect(() => { if (!newKey) { return; @@ -255,28 +260,9 @@ export function StreamKey() { ]} > {didcopy ? ( - - - - - + ) : ( - - - - + )} -- 2.51.2 From a369be3b7fabae25a338d84a0421762df074253c Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Sat, 27 Sep 2025 16:17:47 +0100 Subject: [PATCH 062/525] Add toast I found else where RTMP/Whip button group tidy Remove forced clipboard on key gen --- .../components/live-dashboard/stream-key.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index 2c18e85f..51876d9e 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -1,11 +1,19 @@ -import { Body, Button, Code, Row, View, useTheme } from "@streamplace/components"; +import { + Body, + Button, + Code, + Row, + View, + useTheme, + useToast, +} from "@streamplace/components"; import { Redirect } from "components/aqlink"; import Loading from "components/loading/loading"; import { - clearStreamKeyRecord, - createStreamKeyRecord, - selectIsReady, - selectUserProfile, + clearStreamKeyRecord, + createStreamKeyRecord, + selectIsReady, + selectUserProfile, } from "features/bluesky/blueskySlice"; import { Clipboard, ClipboardCheck } from "lucide-react-native"; import { useEffect, useState } from "react"; @@ -59,12 +67,20 @@ export function StreamKeyScreen() { @@ -172,6 +188,7 @@ export default StreamKeyScreen; export function StreamKey() { const theme = useTheme(); + const toast = useToast(); const dispatch = useAppDispatch(); const [generating, setGenerating] = useState(false); @@ -186,17 +203,6 @@ export function StreamKey() { return; } - (async () => { - try { - await navigator.clipboard.writeText(newKey.privateKey); - // TODO: Replace with custom toast implementation - console.log("Bearer token copied to clipboard"); - } catch (e) { - // not allowed. oh well. - console.log("Could not copy to clipboard"); - } - })(); - return () => { dispatch(clearStreamKeyRecord()); }; @@ -209,12 +215,12 @@ export function StreamKey() { try { await navigator.clipboard.writeText(newKey.privateKey); - // TODO: Replace with custom toast implementation - console.log("Bearer token copied to clipboard"); setDidcopy(true); + + toast.show('Stream Key', 'Stream Key was copied to your clipboard', { duration: 4 }); } catch (e) { // not allowed. oh well. - console.log("Could not copy to clipboard"); + toast.show('Stream Key', 'Failed to copy the Stream Key to your clipboard', { duration: 4 }); } }; -- 2.51.2 From 72f838520d4ef96286fb6cbdb4687b2198638780 Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Sat, 27 Sep 2025 16:23:10 +0100 Subject: [PATCH 063/525] Set vscode settings tabSize to 2 --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5bd61a68..7d459a8b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,6 @@ "editor.codeActionsOnSave": { "source.addMissingImports": "explicit", "source.organizeImports": "explicit" - } + }, + "editor.tabSize": 2 } -- 2.51.2 From 9fb5fb3878f87ae638189c1983a4451eb295ba4c Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sat, 27 Sep 2025 13:05:44 -0500 Subject: [PATCH 064/525] Move deletion action outside portal --- .../src/components/chat/mod-view.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/js/components/src/components/chat/mod-view.tsx b/js/components/src/components/chat/mod-view.tsx index 8254a85a..9360c741 100644 --- a/js/components/src/components/chat/mod-view.tsx +++ b/js/components/src/components/chat/mod-view.tsx @@ -49,6 +49,7 @@ export const ModView = forwardRef(() => { const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen); const setReportSubject = usePlayerStore((x) => x.setReportSubject); const setModMessage = usePlayerStore((x) => x.setModMessage); + const deleteChatMessage = useDeleteChatMessage(); // get the channel did const channelId = usePlayerStore((state) => state.src); @@ -174,7 +175,10 @@ export const ModView = forwardRef(() => { View user on {BSKY_FRONTEND_DOMAIN} {message.author.did === agent?.did && ( - + )} {message.author.did !== agent?.did && ( (() => { ); }); +enum DeleteState { + None, + Confirmed, + Deleting, +} + export function DeleteButton({ message, + deleteChatMessage, }: { message: ChatMessageViewHydrated; + deleteChatMessage: (uri: string) => Promise; }) { - const deleteChatMessage = useDeleteChatMessage(); - const [confirming, setConfirming] = useState(false); + const [confirming, setConfirming] = useState(DeleteState.None); const { onOpenChange } = useRootContext(); return ( { if (!message) return; if (!confirming) { - setConfirming(true); + setConfirming(DeleteState.Confirmed); return; } + if (confirming === DeleteState.Confirmed) { + setConfirming(DeleteState.Deleting); + } deleteChatMessage(message.uri).then(() => { + // wait ~a second before resetting state to allow deletion to take effect + setTimeout(() => setConfirming(DeleteState.None), 1000); onOpenChange?.(false); }); }} > - {confirming ? "Are you sure?" : "Delete message"} + {confirming === DeleteState.Confirmed + ? "Are you sure? Click again to confirm." + : confirming === DeleteState.Deleting + ? "Deleting..." + : "Delete message"} ); -- 2.51.2 From efd697e236cefd25266d77c442683fe2b336af11 Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Sat, 27 Sep 2025 19:07:39 +0100 Subject: [PATCH 065/525] Prettier write --- js/app/components/live-dashboard/stream-key.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/js/app/components/live-dashboard/stream-key.tsx b/js/app/components/live-dashboard/stream-key.tsx index 51876d9e..cbc8a60c 100644 --- a/js/app/components/live-dashboard/stream-key.tsx +++ b/js/app/components/live-dashboard/stream-key.tsx @@ -217,10 +217,16 @@ export function StreamKey() { await navigator.clipboard.writeText(newKey.privateKey); setDidcopy(true); - toast.show('Stream Key', 'Stream Key was copied to your clipboard', { duration: 4 }); + toast.show("Stream Key", "Stream Key was copied to your clipboard", { + duration: 4, + }); } catch (e) { // not allowed. oh well. - toast.show('Stream Key', 'Failed to copy the Stream Key to your clipboard', { duration: 4 }); + toast.show( + "Stream Key", + "Failed to copy the Stream Key to your clipboard", + { duration: 4 }, + ); } }; -- 2.51.2 From c8a2e2822dcdc429221beb918925866e9de7b838 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 27 Sep 2025 12:42:20 -0700 Subject: [PATCH 066/525] v0.7.30 --- js/app/package.json | 2 +- js/components/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index 0a3168c8..4a9516ee 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.29", + "version": "0.7.30", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/components/package.json b/js/components/package.json index 15794d4e..727c3f84 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@streamplace/components", - "version": "0.7.29", + "version": "0.7.30", "description": "Streamplace React (Native) Components", "main": "dist/index.js", "types": "src/index.tsx", diff --git a/js/docs/package.json b/js/docs/package.json index 321bcaae..247b55cd 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.29", + "version": "0.7.30", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index 75185861..b1faf292 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.29", + "version": "0.7.30", "npmClient": "pnpm" } -- 2.51.2 From 347094306a4712db47b0c1e4a2ca2a3d4e48adaf Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 27 Sep 2025 13:32:04 -0700 Subject: [PATCH 067/525] xrpc: buffer thumbnails in memory before upload --- pkg/spxrpc/com_atproto_repo.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/spxrpc/com_atproto_repo.go b/pkg/spxrpc/com_atproto_repo.go index 3e688356..04f75e29 100644 --- a/pkg/spxrpc/com_atproto_repo.go +++ b/pkg/spxrpc/com_atproto_repo.go @@ -1,7 +1,9 @@ package spxrpc import ( + "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -37,6 +39,8 @@ func resolveRepoService(ctx context.Context, repo string) (string, string, strin return did, service, handle, nil } +var maxBlobSize int64 = 1024 * 1024 * 10 // 10MB + func (s *Server) handleComAtprotoRepoUploadBlob(ctx context.Context, r io.Reader, contentType string) (*comatprototypes.RepoUploadBlob_Output, error) { ctx, span := otel.Tracer("server").Start(ctx, "handleComAtprotoRepoUploadBlob") defer span.End() @@ -46,12 +50,20 @@ func (s *Server) handleComAtprotoRepoUploadBlob(ctx context.Context, r io.Reader return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session not found") } + // we need to buffer the blob so we can successfully retry upon dpop nonce changes + var err error + buf := bytes.Buffer{} + _, err = io.CopyN(&buf, r, maxBlobSize+1) + if err == nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "blob size exceeds max size") + } + if !errors.Is(err, io.EOF) { + return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to copy reader to buffer") + } + var out comatprototypes.RepoUploadBlob_Output - var xrpcType string - var err error - xrpcType = xrpc.Procedure - err = client.Do(ctx, xrpcType, contentType, "com.atproto.repo.uploadBlob", nil, r, &out) + err = client.Do(ctx, xrpc.Procedure, contentType, "com.atproto.repo.uploadBlob", nil, bytes.NewReader(buf.Bytes()), &out) if err != nil { log.Error(ctx, "upstream xrpc error", "error", err) -- 2.51.2 From 03fbbd36a10431daedcec11d728805b5d0cd96d5 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 27 Sep 2025 14:13:26 -0700 Subject: [PATCH 068/525] v0.7.31 --- js/app/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index 4a9516ee..496fe0e3 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.30", + "version": "0.7.31", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/docs/package.json b/js/docs/package.json index 247b55cd..1d21524d 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.30", + "version": "0.7.31", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index b1faf292..86037f1a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.30", + "version": "0.7.31", "npmClient": "pnpm" } -- 2.51.2 From 28fff124954e29a0bebbff2ea16e83aca3bede12 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sat, 27 Sep 2025 20:16:34 -0500 Subject: [PATCH 069/525] fix: crash when loading deleteChatMessage when not logged in --- js/components/src/components/chat/mod-view.tsx | 17 ++++++++++++----- js/components/src/livestream-store/chat.tsx | 12 ++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/js/components/src/components/chat/mod-view.tsx b/js/components/src/components/chat/mod-view.tsx index 9360c741..d1453229 100644 --- a/js/components/src/components/chat/mod-view.tsx +++ b/js/components/src/components/chat/mod-view.tsx @@ -21,6 +21,7 @@ import { layout, ResponsiveDropdownMenuContent, Text, + useToast, View, } from "../ui"; @@ -210,6 +211,7 @@ export function DeleteButton({ }) { const [confirming, setConfirming] = useState(DeleteState.None); const { onOpenChange } = useRootContext(); + const toast = useToast(); return ( { @@ -221,11 +223,16 @@ export function DeleteButton({ if (confirming === DeleteState.Confirmed) { setConfirming(DeleteState.Deleting); } - deleteChatMessage(message.uri).then(() => { - // wait ~a second before resetting state to allow deletion to take effect - setTimeout(() => setConfirming(DeleteState.None), 1000); - onOpenChange?.(false); - }); + deleteChatMessage(message.uri) + .then(() => { + // wait ~a second before resetting state to allow deletion to take effect + setTimeout(() => setConfirming(DeleteState.None), 1000); + onOpenChange?.(false); + }) + .catch((e) => { + toast.show("Couldn't delete the message", e); + setConfirming(DeleteState.None); + }); }} > diff --git a/js/components/src/livestream-store/chat.tsx b/js/components/src/livestream-store/chat.tsx index b214adc4..0afc6274 100644 --- a/js/components/src/livestream-store/chat.tsx +++ b/js/components/src/livestream-store/chat.tsx @@ -135,14 +135,14 @@ export const useCreateChatMessage = () => { export const useDeleteChatMessage = () => { const pdsAgent = usePDSAgent(); - if (!pdsAgent) { - throw new Error("No PDS agent found"); - } const userDID = useDID(); - if (!userDID) { - throw new Error("No user DID found"); - } return async (uri: string) => { + if (!pdsAgent) { + throw new Error("No PDS agent found"); + } + if (!userDID) { + throw new Error("No user DID found"); + } const rkey = uri.split("/").pop(); if (!rkey) { throw new Error("No rkey found"); -- 2.51.2 From 8c9f6186d1e718bcc4dfe84fe8c681680dddcf0e Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 27 Sep 2025 18:43:00 -0700 Subject: [PATCH 070/525] v0.7.32 --- js/app/package.json | 2 +- js/components/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index 496fe0e3..ac5b5ea9 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.31", + "version": "0.7.32", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/components/package.json b/js/components/package.json index 727c3f84..43cdf54d 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@streamplace/components", - "version": "0.7.30", + "version": "0.7.32", "description": "Streamplace React (Native) Components", "main": "dist/index.js", "types": "src/index.tsx", diff --git a/js/docs/package.json b/js/docs/package.json index 1d21524d..1234c363 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.31", + "version": "0.7.32", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index 86037f1a..2b4f1f49 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.31", + "version": "0.7.32", "npmClient": "pnpm" } -- 2.51.2 From 332bcc33a20ce428a3a5e75773e2746170d32594 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 29 Sep 2025 12:35:12 -0700 Subject: [PATCH 071/525] spxrpc: remove infinite loop on embedded PDS --- pkg/spxrpc/com_atproto_repo.go | 27 +++++++++++---------------- pkg/spxrpc/spxrpc.go | 13 +++++++++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/pkg/spxrpc/com_atproto_repo.go b/pkg/spxrpc/com_atproto_repo.go index 04f75e29..2db8906a 100644 --- a/pkg/spxrpc/com_atproto_repo.go +++ b/pkg/spxrpc/com_atproto_repo.go @@ -74,13 +74,11 @@ func (s *Server) handleComAtprotoRepoUploadBlob(ctx context.Context, r io.Reader } func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo string) (*comatprototypes.RepoDescribeRepo_Output, error) { - did, svc, handle, err := resolveRepoService(ctx, repo) + isLocal, svc, err := s.isLocalPDS(ctx, repo) if err != nil { - return nil, fmt.Errorf("handleComAtprotoRepoDescribeRepo: %w", err) + return nil, fmt.Errorf("error checking for local PDS: %w", err) } - - // if the service isn't the current host, we proxy the request - if svc != s.cli.PublicHost { + if !isLocal { var out comatprototypes.RepoDescribeRepo_Output params := make(map[string]interface{}) params["repo"] = repo @@ -95,8 +93,8 @@ func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo stri } return &comatprototypes.RepoDescribeRepo_Output{ - Handle: handle, - Did: did, + Handle: s.cli.MyDID(), + Did: s.cli.MyDID(), DidDoc: atproto.DIDDoc(s.cli.PublicHost), Collections: []string{ "com.atproto.lexicon.schema", @@ -106,12 +104,11 @@ func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo stri } func (s *Server) handleComAtprotoRepoListRecords(ctx context.Context, collection string, cursor string, limit int, repo string, reverse *bool) (*comatprototypes.RepoListRecords_Output, error) { - _, svc, _, err := resolveRepoService(ctx, repo) + isLocal, svc, err := s.isLocalPDS(ctx, repo) if err != nil { - return nil, fmt.Errorf("handleComAtprotoRepoListRecords: %w", err) + return nil, fmt.Errorf("error checking for local PDS: %w", err) } - // if the service isn't the current host, we proxy the request - if svc != s.cli.PublicHost { + if !isLocal { var out comatprototypes.RepoListRecords_Output params := make(map[string]interface{}) params["collection"] = collection @@ -161,13 +158,11 @@ func (s *Server) handleComAtprotoRepoListRecords(ctx context.Context, collection } func (s *Server) handleComAtprotoRepoGetRecord(ctx context.Context, c string, collection string, repo string, rkey string) (*comatprototypes.RepoGetRecord_Output, error) { - _, svc, _, err := resolveRepoService(ctx, repo) + isLocal, svc, err := s.isLocalPDS(ctx, repo) if err != nil { - return nil, fmt.Errorf("handleComAtprotoRepoGetRecord: %w", err) + return nil, fmt.Errorf("error checking for local PDS: %w", err) } - - // if the service isn't the current host, we proxy the request - if svc != s.cli.PublicHost { + if !isLocal { var out comatprototypes.RepoGetRecord_Output params := make(map[string]interface{}) params["repo"] = repo diff --git a/pkg/spxrpc/spxrpc.go b/pkg/spxrpc/spxrpc.go index 67ff3025..91c8011d 100644 --- a/pkg/spxrpc/spxrpc.go +++ b/pkg/spxrpc/spxrpc.go @@ -66,7 +66,20 @@ func NewServer(ctx context.Context, cli *config.CLI, model model.Model, stateful return s, nil } +func (s *Server) isLocalPDS(ctx context.Context, repo string) (bool, string, error) { + did, svc, _, err := resolveRepoService(ctx, repo) + if err != nil { + return false, "", fmt.Errorf("resolveRepoService: %w", err) + } + if did == s.cli.MyDID() { + return true, svc, nil + } + return false, svc, nil +} + func makeUnauthenticatedRequest(ctx context.Context, service, method string, params map[string]interface{}, out interface{}) error { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() u, err := url.Parse(fmt.Sprintf("%s/xrpc/%s", service, method)) if err != nil { return fmt.Errorf("failed to parse URL: %w", err) -- 2.51.2 From 6a1f375e0e68da79743b883056cbfee727bbbd36 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 29 Sep 2025 13:21:17 -0700 Subject: [PATCH 072/525] v0.7.33 --- js/app/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index ac5b5ea9..72802fe6 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.32", + "version": "0.7.33", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/docs/package.json b/js/docs/package.json index 1234c363..f1426974 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.32", + "version": "0.7.33", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index 2b4f1f49..d8ef0959 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.32", + "version": "0.7.33", "npmClient": "pnpm" } -- 2.51.2 From 382c7c97d32aadba5d587d558cf2db6d6fbce502 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 29 Sep 2025 16:42:57 -0500 Subject: [PATCH 073/525] Add follow and link to bsky --- js/app/components/follow-button.tsx | 68 ++--- js/app/components/mobile/bottom-metadata.tsx | 28 ++- .../mobile-player/ui/viewer-context-menu.tsx | 101 +++++++- js/components/src/components/ui/button.tsx | 23 +- js/components/src/components/ui/dropdown.tsx | 40 ++- js/components/src/components/ui/icons.tsx | 14 ++ .../src/components/ui/primitives/button.tsx | 7 - js/components/src/lib/theme/tokens.ts | 9 + js/components/src/streamplace-store/graph.tsx | 232 ++++++++++++++++++ 9 files changed, 433 insertions(+), 89 deletions(-) create mode 100644 js/components/src/streamplace-store/graph.tsx diff --git a/js/app/components/follow-button.tsx b/js/app/components/follow-button.tsx index e53356f2..9040a7cc 100644 --- a/js/app/components/follow-button.tsx +++ b/js/app/components/follow-button.tsx @@ -1,6 +1,7 @@ -import { Text, zero } from "@streamplace/components"; +import { Button, Icon, Text, zero } from "@streamplace/components"; +import { Plus } from "lucide-react-native"; import React, { useEffect, useState } from "react"; -import { Pressable, View } from "react-native"; +import { View } from "react-native"; import { followUser, unfollowUser } from "../features/bluesky/blueskySlice"; import { selectStreamplace } from "../features/streamplace/streamplaceSlice"; import { useAppDispatch, useAppSelector } from "../store/hooks"; @@ -111,15 +112,6 @@ const FollowButton: React.FC = ({ } }; - const buttonStyle = [ - zero.bg.transparent, - zero.px[3], - zero.py[2], - zero.r.md, - zero.borders.width.thin, - zero.borders.color.gray[300], - ]; - return ( = ({ zero.gap.all[2], ]} > - {isFollowing === null ? ( - // Skeleton loader to prevent layout shift - -   - - ) : isFollowing ? ( - - - ✓ - Following - - - ) : ( - - - + - Follow - - - )} + {error && {error}} ); diff --git a/js/app/components/mobile/bottom-metadata.tsx b/js/app/components/mobile/bottom-metadata.tsx index 41168fd7..8934172b 100644 --- a/js/app/components/mobile/bottom-metadata.tsx +++ b/js/app/components/mobile/bottom-metadata.tsx @@ -5,12 +5,14 @@ import { ShareSheet, Text, useAvatars, + useDID, useLivestreamInfo, useLivestreamStore, zero, } from "@streamplace/components"; +import FollowButton from "components/follow-button"; import { ChevronLeft, ChevronRight } from "lucide-react-native"; -import { Image, View } from "react-native"; +import { Image, Linking, Pressable, View } from "react-native"; const { gap, px, py, colors } = zero; export function BottomMetadata({ @@ -24,6 +26,8 @@ export function BottomMetadata({ const avatars = useAvatars(profile?.did ? [profile?.did] : []); const ls = useLivestreamStore((x) => x.livestream); + const did = useDID(); + return ( )} - - @{profile?.handle || "user"} - + + { + if (profile?.handle) { + const url = `https://bsky.app/profile/${profile.handle}`; + Linking.openURL(url); + } + }} + > + + @{profile?.handle || "user"} + + + {did && profile && ( + + )} + x.setReportModalOpen); const setReportSubject = usePlayerStore((x) => x.setReportSubject); + const { profile } = useLivestreamInfo(); + const avatars = useAvatars(profile?.did ? [profile?.did] : []); + const ls = useLivestreamStore((x) => x.livestream); + + let graphManager = useGraphManager(profile?.did); + const lowLatency = protocol === "webrtc"; const setLowLatency = (value: boolean) => { setProtocol(value ? PlayerProtocol.WEBRTC : PlayerProtocol.HLS); @@ -61,6 +70,96 @@ export function ContextMenu({ + {Platform.OS !== "web" && ( + + + {profile?.did && avatars[profile?.did]?.avatar && ( + + )} + + + { + if (profile?.handle) { + const url = `https://bsky.app/profile/${profile.handle}`; + Linking.openURL(url); + } + }} + > + @{profile?.handle || "user"} + + {/*{did && profile && ( + + )}*/} + + + {ls?.record.title || "Stream Title"} + + + + + { + try { + if (graphManager.isFollowing) { + await graphManager.unfollow(); + } else { + await graphManager.follow(); + } + } catch (err) { + console.error("Follow/unfollow error:", err); + } + }} + > + + {graphManager.isLoading + ? "Loading..." + : graphManager.isFollowing + ? "Unfollow" + : "Follow"} + + + + { + if (profile?.handle) { + const url = `https://bsky.app/profile/${profile.handle}`; + Linking.openURL(url); + } + }} + > + View Profile on Bluesky + + + )} diff --git a/js/components/src/components/ui/button.tsx b/js/components/src/components/ui/button.tsx index e3a56997..9fa11675 100644 --- a/js/components/src/components/ui/button.tsx +++ b/js/components/src/components/ui/button.tsx @@ -109,7 +109,7 @@ export const Button = forwardRef( { borderRadius: zero.borderRadius.md }, ], inner: { gap: 4 }, - text: zt.text.sm, + text: zero.typography.universal.sm, }; case "lg": return { @@ -118,8 +118,8 @@ export const Button = forwardRef( zero.py[3], { borderRadius: zero.borderRadius.md }, ], - inner: { gap: 8 }, - text: zt.text.lg, + inner: { gap: 12 }, + text: zero.typography.universal.lg, }; case "xl": return { @@ -129,17 +129,17 @@ export const Button = forwardRef( { borderRadius: zero.borderRadius.lg }, ], inner: { gap: 12 }, - text: zt.text.xl, + text: zero.typography.universal.xl, }; case "pill": return { button: [ - zero.px[4], - zero.py[2], + zero.px[2], + zero.py[1], { borderRadius: zero.borderRadius.full }, ], inner: { gap: 4 }, - text: zt.text.sm, + text: zero.typography.universal.xs, }; case "md": default: @@ -150,7 +150,7 @@ export const Button = forwardRef( { borderRadius: zero.borderRadius.md }, ], inner: { gap: 6 }, - text: zt.text.md, + text: zero.typography.universal.sm, }; } }, [size, zt]); @@ -211,15 +211,12 @@ export const Button = forwardRef( ) : leftIcon ? ( - + {leftIcon} ) : null} - + {loading && loadingText ? loadingText : children} diff --git a/js/components/src/components/ui/dropdown.tsx b/js/components/src/components/ui/dropdown.tsx index bdd5b72a..68360a28 100644 --- a/js/components/src/components/ui/dropdown.tsx +++ b/js/components/src/components/ui/dropdown.tsx @@ -13,7 +13,6 @@ import { Platform, Pressable, StyleSheet, - Text, useWindowDimensions, View, } from "react-native"; @@ -39,6 +38,7 @@ import { objectFromObjects, TextContext as TextClassContext, } from "./primitives/text"; +import { Text } from "./text"; export const DropdownMenu = DropdownMenuPrimitive.Root; export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; @@ -87,7 +87,7 @@ export const DropdownMenuBottomSheet = forwardRef< zt.bg.mutedForeground, ]} > - + {typeof children === "function" ? children({ pressed: true }) : children} @@ -285,9 +285,15 @@ export const DropdownMenuItem = forwardRef< pr[2], ]} > - {typeof children === "function" - ? children({ pressed: true }) - : children} + {typeof children === "function" ? ( + children({ pressed: true }) + ) : typeof children === "string" ? ( + + {children} + + ) : ( + children + )}
@@ -384,13 +390,15 @@ export const DropdownMenuLabel = forwardRef< return ( ); @@ -404,7 +412,13 @@ export const DropdownMenuSeparator = forwardRef< return ( ); diff --git a/js/components/src/components/ui/icons.tsx b/js/components/src/components/ui/icons.tsx index 7544e4c9..e85bec7b 100644 --- a/js/components/src/components/ui/icons.tsx +++ b/js/components/src/components/ui/icons.tsx @@ -48,3 +48,17 @@ export function createThemedIcon( ); }; } + +// usage of createThemedIcon +export function Icon({ + icon, + variant = "default", + size = "md", + color, + ...restProps +}: { icon: React.ComponentType } & IconProps) { + const ThemedIcon = createThemedIcon(icon); + return ( + + ); +} diff --git a/js/components/src/components/ui/primitives/button.tsx b/js/components/src/components/ui/primitives/button.tsx index 296582d4..674c6ac7 100644 --- a/js/components/src/components/ui/primitives/button.tsx +++ b/js/components/src/components/ui/primitives/button.tsx @@ -244,7 +244,6 @@ const primitiveStyles = StyleSheet.create({ flexDirection: "row", alignItems: "center", justifyContent: "center", - minHeight: 44, // iOS minimum touch target }, disabled: { opacity: 0.5, @@ -264,12 +263,6 @@ const primitiveStyles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, - iconLeft: { - marginRight: 8, - }, - iconRight: { - marginLeft: 8, - }, iconDisabled: { opacity: 0.5, }, diff --git a/js/components/src/lib/theme/tokens.ts b/js/components/src/lib/theme/tokens.ts index 0d3115a4..b670a916 100644 --- a/js/components/src/lib/theme/tokens.ts +++ b/js/components/src/lib/theme/tokens.ts @@ -590,52 +590,61 @@ export const typography = { }, // Universal typography scale + // Atkinson's center is weird so the marginBottom is there to correct it? universal: { xs: { fontSize: 12, lineHeight: 16, + marginBottom: -0.7, fontWeight: "400" as const, fontFamily: "AtkinsonHyperlegibleNext-Regular", }, sm: { fontSize: 14, lineHeight: 20, + marginBottom: -1, fontWeight: "400" as const, fontFamily: "AtkinsonHyperlegibleNext-Regular", }, base: { fontSize: 16, lineHeight: 24, + marginBottom: -1.2, fontWeight: "400" as const, fontFamily: "AtkinsonHyperlegibleNext-Regular", }, lg: { fontSize: 18, lineHeight: 28, + marginBottom: -1.5, fontWeight: "400" as const, fontFamily: "AtkinsonHyperlegibleNext-Regular", }, xl: { fontSize: 20, lineHeight: 28, + marginBottom: -1.75, fontWeight: "500" as const, fontFamily: "AtkinsonHyperlegibleNext-Medium", }, "2xl": { fontSize: 24, lineHeight: 32, + marginBottom: -2, fontWeight: "600" as const, fontFamily: "AtkinsonHyperlegibleNext-SemiBold", }, "3xl": { fontSize: 30, lineHeight: 36, + marginBottom: -2.5, fontWeight: "700" as const, fontFamily: "AtkinsonHyperlegibleNext-Bold", }, "4xl": { fontSize: 36, lineHeight: 40, + marginBottom: -3, fontWeight: "700" as const, fontFamily: "AtkinsonHyperlegibleNext-ExtraBold", }, diff --git a/js/components/src/streamplace-store/graph.tsx b/js/components/src/streamplace-store/graph.tsx new file mode 100644 index 00000000..288fced1 --- /dev/null +++ b/js/components/src/streamplace-store/graph.tsx @@ -0,0 +1,232 @@ +import { AppBskyGraphFollow } from "@atproto/api"; +import { useEffect, useState } from "react"; +import { useStreamplaceStore } from "./streamplace-store"; +import { usePDSAgent } from "./xrpc"; + +export function useCreateFollowRecord() { + let agent = usePDSAgent(); + const [isLoading, setIsLoading] = useState(false); + + const createFollow = async (subjectDID: string) => { + if (!agent) { + throw new Error("No PDS agent found"); + } + + if (!agent.did) { + throw new Error("No user DID found, assuming not logged in"); + } + + setIsLoading(true); + try { + const record: AppBskyGraphFollow.Record = { + $type: "app.bsky.graph.follow", + subject: subjectDID, + createdAt: new Date().toISOString(), + }; + const result = await agent.com.atproto.repo.createRecord({ + repo: agent.did, + collection: "app.bsky.graph.follow", + record, + }); + return result; + } finally { + setIsLoading(false); + } + }; + + return { createFollow, isLoading }; +} + +export function useDeleteFollowRecord() { + let agent = usePDSAgent(); + const [isLoading, setIsLoading] = useState(false); + + const deleteFollow = async (followRecordUri: string) => { + if (!agent) { + throw new Error("No PDS agent found"); + } + + if (!agent.did) { + throw new Error("No user DID found, assuming not logged in"); + } + + setIsLoading(true); + try { + const result = await agent.com.atproto.repo.deleteRecord({ + repo: agent.did, + collection: "app.bsky.graph.follow", + rkey: followRecordUri.split("/").pop()!, + }); + return result; + } finally { + setIsLoading(false); + } + }; + + return { deleteFollow, isLoading }; +} + +interface GraphManagerState { + isFollowing: boolean | null; + followUri: string | null; + isLoading: boolean; + error: string | null; +} + +interface GraphManagerActions { + follow: () => Promise; + unfollow: () => Promise; + refresh: () => Promise; +} + +export function useGraphManager( + subjectDID: string | null | undefined, +): GraphManagerState & GraphManagerActions { + const agent = usePDSAgent(); + const [isFollowing, setIsFollowing] = useState(null); + const [followUri, setFollowUri] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const userDID = agent?.did; + + const streamplaceUrl = useStreamplaceStore((state) => state.url); + + const fetchFollowStatus = async () => { + if (!userDID || !subjectDID || !streamplaceUrl) { + setIsFollowing(null); + setFollowUri(null); + return; + } + + setIsLoading(true); + setError(null); + try { + const res = await fetch( + `${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(subjectDID)}&userDID=${encodeURIComponent(userDID)}`, + { + credentials: "include", + }, + ); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to fetch follow status: ${errorText}`); + } + + const data = await res.json(); + + if (data.follow) { + setIsFollowing(true); + setFollowUri(data.follow.uri); + } else { + setIsFollowing(false); + setFollowUri(null); + } + } catch (err) { + setError( + `Could not determine follow state: ${err instanceof Error ? err.message : "Unknown error"}`, + ); + setIsFollowing(null); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!userDID || !subjectDID) { + setIsFollowing(null); + setFollowUri(null); + setError(null); + return; + } + + fetchFollowStatus(); + }, [userDID, subjectDID, streamplaceUrl]); + + const follow = async () => { + if (!agent || !subjectDID) { + throw new Error("Cannot follow: not logged in or no subject DID"); + } + + if (!agent.did) { + throw new Error("No user DID found, assuming not logged in"); + } + + setIsLoading(true); + setError(null); + const previousState = isFollowing; + setIsFollowing(true); // Optimistic + + try { + const record: AppBskyGraphFollow.Record = { + $type: "app.bsky.graph.follow", + subject: subjectDID, + createdAt: new Date().toISOString(), + }; + const result = await agent.com.atproto.repo.createRecord({ + repo: agent.did, + collection: "app.bsky.graph.follow", + record, + }); + setFollowUri(result.data.uri); + setIsFollowing(true); + } catch (err) { + setIsFollowing(previousState); + const errorMsg = `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`; + setError(errorMsg); + throw new Error(errorMsg); + } finally { + setIsLoading(false); + } + }; + + const unfollow = async () => { + if (!agent || !subjectDID) { + throw new Error("Cannot unfollow: not logged in or no subject DID"); + } + + if (!agent.did) { + throw new Error("No user DID found, assuming not logged in"); + } + + if (!followUri) { + throw new Error("Cannot unfollow: no follow URI found"); + } + + setIsLoading(true); + setError(null); + const previousState = isFollowing; + const previousUri = followUri; + setIsFollowing(false); // Optimistic + setFollowUri(null); + + try { + await agent.com.atproto.repo.deleteRecord({ + repo: agent.did, + collection: "app.bsky.graph.follow", + rkey: followUri.split("/").pop()!, + }); + setIsFollowing(false); + setFollowUri(null); + } catch (err) { + setIsFollowing(previousState); + setFollowUri(previousUri); + const errorMsg = `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`; + setError(errorMsg); + throw new Error(errorMsg); + } finally { + setIsLoading(false); + } + }; + + return { + isFollowing, + followUri, + isLoading, + error, + follow, + unfollow, + refresh: fetchFollowStatus, + }; +} -- 2.51.2 From bcda16cc7acb1cd84af19b610d2c489865c0c821 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 30 Sep 2025 11:35:27 +0300 Subject: [PATCH 074/525] Add code from iroh-smol-kv, streamplace branch and adapt --- Cargo.lock | 262 +++++-- rust/export-c2pa-schema/src/main.rs | 3 +- rust/iroh-streamplace/Cargo.toml | 13 +- rust/iroh-streamplace/src/c2pa.rs | 9 +- rust/iroh-streamplace/src/endpoint.rs | 3 +- rust/iroh-streamplace/src/key.rs | 24 +- rust/iroh-streamplace/src/lib.rs | 1 + rust/iroh-streamplace/src/node/db.rs | 615 +++++++++++++++ .../src/node/db/kv/public_key.rs | 102 +++ .../src/node/db/kv/subscribe_mode.rs | 29 + .../src/node/db/kv/time_bound.rs | 29 + rust/iroh-streamplace/src/node/mod.rs | 13 + rust/iroh-streamplace/src/node/streams.rs | 734 ++++++++++++++++++ rust/iroh-streamplace/src/node/tests.rs | 299 +++++++ rust/iroh-streamplace/src/receiver.rs | 27 +- rust/iroh-streamplace/src/sender.rs | 5 +- rust/iroh-streamplace/src/utils.rs | 12 +- 17 files changed, 2085 insertions(+), 95 deletions(-) create mode 100644 rust/iroh-streamplace/src/node/db.rs create mode 100644 rust/iroh-streamplace/src/node/db/kv/public_key.rs create mode 100644 rust/iroh-streamplace/src/node/db/kv/subscribe_mode.rs create mode 100644 rust/iroh-streamplace/src/node/db/kv/time_bound.rs create mode 100644 rust/iroh-streamplace/src/node/mod.rs create mode 100644 rust/iroh-streamplace/src/node/streams.rs create mode 100644 rust/iroh-streamplace/src/node/tests.rs diff --git a/Cargo.lock b/Cargo.lock index c775baed..f60da078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,12 +76,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -109,6 +103,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "archery" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" +dependencies = [ + "triomphe", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -676,17 +679,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -1368,6 +1370,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.2" @@ -1467,6 +1475,21 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb68017df91f2e477ed4bea586c59eaecaa47ed885a770d0444e21e62572cd2" +dependencies = [ + "fixedbitset", + "futures-buffered", + "futures-core", + "futures-lite", + "pin-project", + "slab", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -2225,9 +2248,9 @@ dependencies = [ [[package]] name = "iroh" -version = "0.91.1" +version = "0.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a98c47bb5f720edeb77be502a8acd238a3c0755f0b1ad865a716224d794a59" +checksum = "135ad6b793a5851b9e5435ad36fea63df485f8fd4520a58117e7dc3326a69c15" dependencies = [ "aead", "backon", @@ -2255,7 +2278,7 @@ dependencies = [ "n0-snafu", "n0-watcher", "nested_enum_utils", - "netdev", + "netdev 0.36.0", "netwatch", "pin-project", "pkarr", @@ -2287,9 +2310,9 @@ dependencies = [ [[package]] name = "iroh-base" -version = "0.91.1" +version = "0.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bde4e612191173e8ade55e3aa719044514edfff952292ffbf581be35cbb59c" +checksum = "04ae51a14c9255a735b1db2d8cf29b875b971e96a5b23e4d0d1ee7d85bf32132" dependencies = [ "curve25519-dalek", "data-encoding", @@ -2297,12 +2320,46 @@ dependencies = [ "ed25519-dalek", "n0-snafu", "nested_enum_utils", + "postcard", "rand_core 0.6.4", "serde", "snafu", "url", ] +[[package]] +name = "iroh-gossip" +version = "0.92.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d530166ebdcfe321fb3a406dba62d777c98e1159237c7aaf0591e3758eb013c" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more 1.0.0", + "ed25519-dalek", + "futures-concurrency", + "futures-lite", + "futures-util", + "hex", + "indexmap 2.10.0", + "iroh", + "iroh-base", + "iroh-metrics", + "irpc", + "n0-future 0.1.3", + "n0-snafu", + "nested_enum_utils", + "postcard", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "snafu", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "iroh-metrics" version = "0.35.0" @@ -2385,9 +2442,9 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "0.91.1" +version = "0.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f19e43de2cfc04748054e1cbf7854ae1e192ddc45a4adeae7be7076a8f1fd43" +checksum = "315cb02e660de0de339303296df9a29b27550180bb3979d0753a267649b34a7f" dependencies = [ "blake3", "bytes", @@ -2433,6 +2490,32 @@ dependencies = [ "z32", ] +[[package]] +name = "iroh-smol-kv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf263d8aba7c7ec5baddcec76d9a6881b3f5470d293707b3881545f9548f31c" +dependencies = [ + "bytes", + "chrono", + "ed25519-dalek", + "hex", + "iroh", + "iroh-gossip", + "irpc", + "n0-future 0.2.0", + "peg", + "postcard", + "rand 0.8.5", + "rpds", + "serde", + "serde-big-array", + "snafu", + "sync_wrapper", + "tokio", + "tracing", +] + [[package]] name = "iroh-streamplace" version = "0.1.0" @@ -2441,14 +2524,21 @@ dependencies = [ "async-trait", "bytes", "c2pa", + "hex", "iroh", + "iroh-base", + "iroh-gossip", + "iroh-smol-kv", "irpc", "irpc-iroh", "n0-future 0.2.0", "postcard", + "rand 0.8.5", + "ref-cast", "serde", "serde_json", "snafu", + "testresult", "thiserror 2.0.16", "tokio", "tracing", @@ -2459,9 +2549,9 @@ dependencies = [ [[package]] name = "irpc" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f8f1d0987ea9da3d74698f921d0a817a214c83b2635a33ed4bc3efa4de1acd" +checksum = "092c0b20697bbc7de4839eebcb49be975cc09221021626d301eea55fc10bfeb7" dependencies = [ "anyhow", "futures-buffered", @@ -2482,9 +2572,9 @@ dependencies = [ [[package]] name = "irpc-derive" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0b26b834d401a046dd9d47bc236517c746eddbb5d25ff3e1a6075bfa4eebdb" +checksum = "209d38d83c0f7043916e90de2d3a8d01035db3a2f49ea7d5fb41b8f43e889924" dependencies = [ "proc-macro2", "quote", @@ -2493,13 +2583,14 @@ dependencies = [ [[package]] name = "irpc-iroh" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5926531af491c6962db4d79f43ea219404cb800889922a728b1d3b92f887eda" +checksum = "dba5a31134415cdcfe65c309b4d3a9560764ca938e98e8717a25d3973f141ac1" dependencies = [ "anyhow", "getrandom 0.3.3", "iroh", + "iroh-base", "irpc", "n0-future 0.1.3", "postcard", @@ -2785,9 +2876,9 @@ dependencies = [ [[package]] name = "n0-snafu" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fed465ff57041f29db78a9adc8864296ef93c6c16029f9e192dc303404ebd0" +checksum = "1815107e577a95bfccedb4cfabc73d709c0db6d12de3f14e0f284a8c5036dc4f" dependencies = [ "anyhow", "btparse", @@ -2836,6 +2927,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "netdev" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa1e3eaf125c54c21e6221df12dd2a0a682784a068782dd564c836c0f281b6d" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.22.0", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -2918,9 +3026,9 @@ dependencies = [ [[package]] name = "netwatch" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901dbb408894af3df3fc51420ba0c6faf3a7d896077b797c39b7001e2f787bd" +checksum = "8a63d76f52f3f15ebde3ca751a2ab73a33ae156662bc04383bac8e824f84e9bb" dependencies = [ "atomic-waker", "bytes", @@ -2932,7 +3040,7 @@ dependencies = [ "n0-future 0.1.3", "n0-watcher", "nested_enum_utils", - "netdev", + "netdev 0.37.3", "netlink-packet-core", "netlink-packet-route 0.24.0", "netlink-proto", @@ -3267,6 +3375,33 @@ dependencies = [ "utf8-decode", ] +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + [[package]] name = "pem" version = "3.0.5" @@ -3522,9 +3657,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portmapper" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f1975debe62a70557e42b9ff9466e4890cf9d3d156d296408a711f1c5f642b" +checksum = "a9f99e8cd25cd8ee09fc7da59357fd433c0a19272956ebb4ad7443b21842988d" dependencies = [ "base64 0.22.1", "bytes", @@ -3971,18 +4106,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -4118,6 +4253,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rpds" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f89f654d51fffdd6026289d07d1fd523244d46ae0a8bc22caa6dd7f9e8cb0b" +dependencies = [ + "archery", + "serde", +] + [[package]] name = "rsa" version = "0.9.8" @@ -4369,6 +4514,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-transcode" version = "1.1.1" @@ -4631,9 +4785,9 @@ checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" [[package]] name = "snafu" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" dependencies = [ "backtrace", "snafu-derive", @@ -4641,9 +4795,9 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ "heck", "proc-macro2", @@ -4925,6 +5079,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "testresult" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614b328ff036a4ef882c61570f72918f7e9c5bee1da33f8e7f91e01daee7e56c" + [[package]] name = "textwrap" version = "0.16.2" @@ -5321,6 +5481,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" + [[package]] name = "try-lock" version = "0.2.5" @@ -5813,7 +5979,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -5834,7 +6000,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -5846,7 +6012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -5878,6 +6044,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -5885,7 +6057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5894,7 +6066,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5903,7 +6075,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5970,7 +6142,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] diff --git a/rust/export-c2pa-schema/src/main.rs b/rust/export-c2pa-schema/src/main.rs index ee1e4f32..b115a9c6 100644 --- a/rust/export-c2pa-schema/src/main.rs +++ b/rust/export-c2pa-schema/src/main.rs @@ -2,8 +2,7 @@ use std::{fs, path::Path}; use anyhow::Result; use c2pa::{Builder, ManifestDefinition, Reader}; -use schemars::JsonSchema; -use schemars::{schema::RootSchema, schema_for}; +use schemars::{schema::RootSchema, schema_for, JsonSchema}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; diff --git a/rust/iroh-streamplace/Cargo.toml b/rust/iroh-streamplace/Cargo.toml index c99de17f..d67ce3ee 100644 --- a/rust/iroh-streamplace/Cargo.toml +++ b/rust/iroh-streamplace/Cargo.toml @@ -8,7 +8,7 @@ name = "iroh_streamplace" crate-type = ["staticlib", "cdylib"] [dependencies] -iroh = { version = "0.91.1", features = ["discovery-local-network"] } +iroh = { version = "0.92", features = ["discovery-local-network"] } tokio = "1.47.1" uniffi = { version = "=0.28.3", features = ["tokio"] } @@ -21,14 +21,21 @@ snafu = "0.8.6" anyhow = "1.0.99" serde = { version = "1.0.219", features = ["derive"] } postcard = { version = "1.1.3", features = ["use-std"] } -irpc = "0.7.0" -irpc-iroh = "0.7.0" +irpc = "0.8.0" +irpc-iroh = "0.8.0" c2pa = { git = "https://github.com/hyphacoop/c2pa-rs.git", rev = "1b84d40219b27340a30fc309250e774e8a7b7761", features = [ "openssl", "file_io", ] } thiserror = "2.0.16" serde_json = "1.0.145" +iroh-smol-kv = "0.2.0" +hex = "0.4.3" +iroh-gossip = "0.92.0" +ref-cast = "1.0.25" +iroh-base = "0.92.0" +rand = "0.8.5" [dev-dependencies] +testresult = "0.4.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/rust/iroh-streamplace/src/c2pa.rs b/rust/iroh-streamplace/src/c2pa.rs index 73110929..289d267b 100644 --- a/rust/iroh-streamplace/src/c2pa.rs +++ b/rust/iroh-streamplace/src/c2pa.rs @@ -1,10 +1,7 @@ -use c2pa::Builder; -use c2pa::CallbackSigner; -use c2pa::Reader; -use c2pa::settings::Settings; +use std::{io::Cursor, sync::Arc}; + +use c2pa::{Builder, CallbackSigner, Reader, settings::Settings}; use serde_json; -use std::io::Cursor; -use std::sync::Arc; #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] diff --git a/rust/iroh-streamplace/src/endpoint.rs b/rust/iroh-streamplace/src/endpoint.rs index 56e8e0e1..f81c5bf8 100644 --- a/rust/iroh-streamplace/src/endpoint.rs +++ b/rust/iroh-streamplace/src/endpoint.rs @@ -1,7 +1,6 @@ use iroh::Watcher; -use crate::error::Error; -use crate::utils::NodeAddr; +use crate::{error::Error, utils::NodeAddr}; #[derive(uniffi::Object, Debug, Clone)] pub struct Endpoint { diff --git a/rust/iroh-streamplace/src/key.rs b/rust/iroh-streamplace/src/key.rs index 37fab52f..423b53a2 100644 --- a/rust/iroh-streamplace/src/key.rs +++ b/rust/iroh-streamplace/src/key.rs @@ -8,27 +8,27 @@ use crate::error::{Error, InvalidPublicKeySnafu}; /// information that is cached for performance reasons. #[derive(Debug, Clone, Eq, uniffi::Object)] #[uniffi::export(Display)] -pub struct PublicKey { +pub struct PublicKeyOld { pub(crate) key: [u8; 32], } -impl From for PublicKey { +impl From for PublicKeyOld { fn from(key: iroh::PublicKey) -> Self { - PublicKey { + PublicKeyOld { key: *key.as_bytes(), } } } -impl From<&PublicKey> for iroh::PublicKey { - fn from(key: &PublicKey) -> Self { +impl From<&PublicKeyOld> for iroh::PublicKey { + fn from(key: &PublicKeyOld) -> Self { iroh::PublicKey::from_bytes(&key.key).unwrap() } } #[uniffi::export] -impl PublicKey { +impl PublicKeyOld { /// Returns true if the PublicKeys are equal - pub fn equal(&self, other: &PublicKey) -> bool { + pub fn equal(&self, other: &PublicKeyOld) -> bool { *self == *other } @@ -62,13 +62,13 @@ impl PublicKey { } } -impl PartialEq for PublicKey { - fn eq(&self, other: &PublicKey) -> bool { +impl PartialEq for PublicKeyOld { + fn eq(&self, other: &PublicKeyOld) -> bool { self.key == other.key } } -impl std::fmt::Display for PublicKey { +impl std::fmt::Display for PublicKeyOld { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { iroh::PublicKey::from(self).fmt(f) } @@ -86,7 +86,7 @@ mod tests { let bytes = b"\x52\x3c\x79\x96\xba\xd7\x74\x24\xe9\x67\x86\xcf\x7a\x72\x05\x11\x53\x37\xa5\xb4\x56\x5c\xd2\x55\x06\xa0\xf2\x97\xb1\x91\xa5\xea"; // // create key from string - let key = PublicKey::from_string(key_str.clone()).unwrap(); + let key = PublicKeyOld::from_string(key_str.clone()).unwrap(); // // test methods are as expected assert_eq!(key_str, key.to_string()); @@ -94,7 +94,7 @@ mod tests { assert_eq!(fmt_str, key.fmt_short()); // // create key from bytes - let key_0 = PublicKey::from_bytes(bytes.to_vec()).unwrap(); + let key_0 = PublicKeyOld::from_bytes(bytes.to_vec()).unwrap(); // // test methods are as expected assert_eq!(key_str, key_0.to_string()); diff --git a/rust/iroh-streamplace/src/lib.rs b/rust/iroh-streamplace/src/lib.rs index 11534766..5a1dcac5 100644 --- a/rust/iroh-streamplace/src/lib.rs +++ b/rust/iroh-streamplace/src/lib.rs @@ -4,6 +4,7 @@ pub mod c2pa; pub mod endpoint; pub mod error; pub mod key; +pub mod node; pub mod receiver; pub mod sender; pub mod utils; diff --git a/rust/iroh-streamplace/src/node/db.rs b/rust/iroh-streamplace/src/node/db.rs new file mode 100644 index 00000000..e0c42bfe --- /dev/null +++ b/rust/iroh-streamplace/src/node/db.rs @@ -0,0 +1,615 @@ +use std::{ + fmt::{self, Debug}, + ops::Bound, + pin::Pin, + sync::Arc, + time::Duration, +}; + +use bytes::Bytes; +use n0_future::{Stream, StreamExt}; +use ref_cast::RefCast; +use snafu::Snafu; +use tokio::sync::Mutex; + +use super::db; + +// the files here are just copied from iroh-smol-kv-uniffi/src/code +mod kv { + mod public_key; + pub use public_key::PublicKey; + mod time_bound; + pub use time_bound::TimeBound; + mod subscribe_mode; + pub use subscribe_mode::SubscribeMode; +} +use db::util::format_bytes; +pub use kv::{PublicKey, SubscribeMode, TimeBound}; + +/// Error creating a new database node. +#[derive(Debug, Snafu, uniffi::Error)] +#[snafu(module)] +pub enum CreateError { + /// The provided private key is invalid (not 32 bytes). + PrivateKey { size: u64 }, + /// The provided gossip topic is invalid (not 32 bytes). + Topic { size: u64 }, + /// Failed to bind the iroh endpoint. + Bind { message: String }, + /// Failed to subscribe to the gossip topic. + Subscribe { message: String }, +} + +/// Error joining peers. +#[derive(Debug, Snafu, uniffi::Error)] +#[snafu(module)] +pub enum JoinPeersError { + /// Failed to parse a provided iroh node ticket. + Ticket { message: String }, + /// Error during the join peers operation. + Irpc { message: String }, +} + +/// Error putting a value into the database. +#[derive(Debug, Snafu, uniffi::Error)] +#[snafu(module)] +pub enum PutError { + /// Error during the put operation. + Irpc { message: String }, +} + +/// Configuration for an iroh-streamplace node. +#[derive(uniffi::Record, Clone)] +pub struct Config { + /// An Ed25519 secret key as a 32 byte array. + pub key: Vec, + /// The gossip topic to use. Must be 32 bytes. + /// + /// You can use e.g. a BLAKE3 hash of a topic string here. This can be used + /// as a cheap way to have a shared secret - nodes that do not know the topic + /// cannot connect to the swarm. + pub topic: Vec, + /// Maximum duration to wait for sending a stream piece to a peer. + pub max_send_duration: Duration, + /// Disable using relays, for tests. + pub disable_relay: bool, +} + +#[derive(uniffi::Enum, Debug, Clone)] +enum StreamFilter { + All, + Global, + Stream(Vec), +} + +/// A filter for subscriptions and iteration. +#[derive(uniffi::Object, Debug, Clone)] +pub struct Filter { + scope: Option>>, + stream: StreamFilter, + min_time: TimeBound, + max_time: TimeBound, +} + +#[uniffi::export] +impl Filter { + /// Creates a new filter that matches everything. + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self { + scope: None, + stream: StreamFilter::All, + min_time: TimeBound::Unbounded, + max_time: TimeBound::Unbounded, + }) + } + + /// Restrict to the global namespace, no per stream data. + pub fn global(mut self: Arc) -> Arc { + let this = Arc::make_mut(&mut self); + this.stream = StreamFilter::Global; + self + } + + /// Restrict to one specific stream, no global data. + pub fn stream(mut self: Arc, stream: Vec) -> Arc { + let this = Arc::make_mut(&mut self); + this.stream = StreamFilter::Stream(stream); + self + } + + /// Restrict to a set of scopes. + pub fn scopes(mut self: Arc, scopes: Vec>) -> Arc { + let this = Arc::make_mut(&mut self); + this.scope = Some(scopes); + self + } + + /// Restrict to a single scope. + pub fn scope(self: Arc, scope: Arc) -> Arc { + self.scopes(vec![scope]) + } + + /// Restrict to a time range. + pub fn timestamps(mut self: Arc, min: TimeBound, max: TimeBound) -> Arc { + let this = Arc::make_mut(&mut self); + this.min_time = min; + this.max_time = max; + self + } + + /// Restrict to a time range given in nanoseconds since unix epoch. + pub fn time_range(self: Arc, min: u64, max: u64) -> Arc { + self.timestamps(TimeBound::Included(min), TimeBound::Excluded(max)) + } + + /// Restrict to a time range starting at min, unbounded at the top. + pub fn time_from(self: Arc, min: u64) -> Arc { + self.timestamps(TimeBound::Included(min), TimeBound::Unbounded) + } +} + +impl From for iroh_smol_kv::Filter { + fn from(value: Filter) -> Self { + let mut filter = iroh_smol_kv::Filter::ALL; + match value.stream { + // everything + StreamFilter::All => {} + // everything starting with 'g', for the global namespace + StreamFilter::Global => { + filter = filter.key_prefix(b"g".as_ref()); + } + // a specific stream, everything starting with 's' + escaped stream name + StreamFilter::Stream(t) => { + let prefix = util::encode_stream_and_key(Some(&t), &[]); + filter = filter.key_prefix(prefix); + } + }; + filter = filter.timestamps_nanos(( + Bound::::from(value.min_time), + Bound::::from(value.max_time), + )); + if let Some(scopes) = value.scope { + let keys = scopes.iter().map(|k| iroh::PublicKey::from(k.as_ref())); + filter = filter.scopes(keys); + } + filter + } +} + +/// Error getting the next item from a subscription. +#[derive(uniffi::Enum, Snafu, Debug)] +#[snafu(module)] +pub enum SubscribeNextError { + /// Error during the subscribe next operation. + Irpc { message: String }, +} + +/// Error getting the next item from a subscription. +#[derive(uniffi::Enum, Snafu, Debug)] +#[snafu(module)] +pub enum WriteError { + /// The provided private key is invalid (not 32 bytes). + PrivateKeySize { size: u64 }, +} + +/// An entry returned from the database. +#[derive(uniffi::Record, Debug, PartialEq, Eq)] +pub struct Entry { + scope: Arc, + stream: Option>, + key: Vec, + value: Vec, + timestamp: u64, +} + +/// An item returned from a subscription. +#[derive(uniffi::Enum, Debug)] +pub enum SubscribeItem { + Entry { + scope: Arc, + stream: Option>, + key: Vec, + value: Vec, + timestamp: u64, + }, + CurrentDone, + Expired { + scope: Arc, + stream: Option>, + key: Vec, + timestamp: u64, + }, + Other, +} + +fn fmt_stream(stream: &Option>) -> String { + match stream { + None => "".to_string(), + Some(s) => format_bytes(s), + } +} + +#[uniffi::export] +pub fn subscribe_item_debug(item: &SubscribeItem) -> String { + match item { + SubscribeItem::Entry { + scope, + stream, + key, + value, + timestamp, + } => format!( + "Entry {{ scope: {}, stream: {}, key: {}, value: {}, timestamp: {} }}", + scope.fmt_short(), + fmt_stream(stream), + format_bytes(key), + format_bytes(value), + timestamp + ), + SubscribeItem::CurrentDone => "CurrentDone".to_string(), + SubscribeItem::Expired { + scope, + stream, + key, + timestamp, + } => format!( + "Expired {{ scope: {}, stream: {}, key: {}, timestamp: {} }}", + scope.fmt_short(), + fmt_stream(stream), + format_bytes(key), + timestamp + ), + SubscribeItem::Other => "Other".to_string(), + } +} + +impl From for SubscribeItem { + fn from(item: iroh_smol_kv::SubscribeItem) -> Self { + match &item { + iroh_smol_kv::SubscribeItem::Entry((scope, key, value)) => { + let Some((stream, key)) = util::decode_stream_and_key(key) else { + return Self::Other; + }; + Self::Entry { + scope: Arc::new((*scope).into()), + stream, + key, + value: value.value.to_vec(), + timestamp: value.timestamp, + } + } + iroh_smol_kv::SubscribeItem::CurrentDone => Self::CurrentDone, + iroh_smol_kv::SubscribeItem::Expired((scope, topic, timestamp)) => { + let (stream, key) = util::decode_stream_and_key(topic).unwrap(); + Self::Expired { + scope: Arc::new((*scope).into()), + stream, + key, + timestamp: *timestamp, + } + } + } + } +} + +/// A response to a subscribe request. +/// +/// This can be used as a stream of [`SubscribeItem`]s. +#[derive(uniffi::Object)] +#[uniffi::export(Debug)] +#[allow(clippy::type_complexity)] +pub struct SubscribeResponse { + inner: Mutex< + Pin< + Box< + dyn Stream> + + Send + + Sync + + 'static, + >, + >, + >, +} + +impl fmt::Debug for SubscribeResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SubscribeResponse").finish_non_exhaustive() + } +} + +#[uniffi::export] +impl SubscribeResponse { + pub async fn next_raw(&self) -> Result, SubscribeNextError> { + let mut this = self.inner.lock().await; + match this.as_mut().next().await { + None => Ok(None), + Some(Ok(item)) => Ok(Some(item.into())), + Some(Err(e)) => Err(SubscribeNextError::Irpc { + message: e.to_string(), + }), + } + } +} + +/// Options for subscribing. +/// +/// `filter` specifies what to subscribe to. +/// `mode` specifies whether to get current items, new items, or both. +#[derive(uniffi::Record)] +pub struct SubscribeOpts { + pub filter: Arc, + pub mode: SubscribeMode, +} + +impl From for iroh_smol_kv::Subscribe { + fn from(opts: SubscribeOpts) -> Self { + iroh_smol_kv::Subscribe { + filter: opts.filter.as_ref().clone().into(), + mode: opts.mode.into(), + } + } +} + +/// A write scope that can be used to put values into the database. +/// +/// The default write scope is available from the [`Node::node_scope`] method. +#[derive(Clone, Debug, RefCast, uniffi::Object)] +#[repr(transparent)] +pub struct WriteScope(iroh_smol_kv::WriteScope); + +#[uniffi::export] +impl WriteScope { + pub async fn put( + &self, + stream: Option>, + key: Vec, + value: Vec, + ) -> Result<(), PutError> { + self.put_impl(stream, key, value.into()) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + }) + } +} + +impl WriteScope { + pub fn new(inner: iroh_smol_kv::WriteScope) -> Self { + Self(inner) + } + + /// Put a value into the database, optionally in a specific stream. + pub async fn put_impl( + &self, + stream: Option>, + key: impl AsRef<[u8]>, + value: Bytes, + ) -> Result<(), irpc::Error> { + let key = key.as_ref(); + let stream = stream.as_ref().map(|s| s.as_ref()); + let encoded = util::encode_stream_and_key(stream, key); + self.0.put(encoded, value).await?; + Ok(()) + } +} + +/// Iroh-streamplace specific metadata database. +#[derive(Debug, Clone, RefCast, uniffi::Object)] +#[repr(transparent)] +pub struct Db(iroh_smol_kv::Client); + +impl Db { + pub fn new(inner: iroh_smol_kv::Client) -> Self { + Self(inner) + } + + pub fn inner(&self) -> &iroh_smol_kv::Client { + &self.0 + } +} + +#[uniffi::export] +impl Db { + pub fn write(&self, secret: Vec) -> Result, WriteError> { + let secret = iroh::SecretKey::from_bytes(&secret.try_into().map_err(|e: Vec| { + WriteError::PrivateKeySize { + size: e.len() as u64, + } + })?); + let write = self.0.write(secret); + Ok(Arc::new(WriteScope::new(write))) + } + + pub async fn iter_with_opts( + &self, + filter: Arc, + ) -> Result, SubscribeNextError> { + let sub = self.subscribe_with_opts(SubscribeOpts { + filter, + mode: SubscribeMode::Current, + }); + let mut items = Vec::new(); + while let Some(item) = sub.next_raw().await? { + match item { + SubscribeItem::Entry { + scope, + stream, + key, + value, + timestamp, + } => { + items.push(Entry { + scope, + stream, + key, + value, + timestamp, + }); + } + _ => unreachable!("we used SubscribeMode::Current, so we should only get entries"), + } + } + Ok(items) + } + + pub fn subscribe(&self, filter: Arc) -> Arc { + self.subscribe_with_opts(SubscribeOpts { + filter, + mode: SubscribeMode::Both, + }) + } + + /// Subscribe with options. + pub fn subscribe_with_opts(&self, opts: SubscribeOpts) -> Arc { + Arc::new(SubscribeResponse { + inner: Mutex::new(Box::pin( + self.0.subscribe_with_opts(opts.into()).stream_raw(), + )), + }) + } +} + +mod util { + + pub fn encode_stream_and_key(stream: Option<&[u8]>, key: &[u8]) -> Vec { + let mut result = Vec::new(); + if let Some(s) = stream { + result.push(b's'); + escape_into([s], &mut result); + } else { + result.push(b'g'); + } + result.extend(key); + result + } + + pub fn decode_stream_and_key(encoded: &[u8]) -> Option<(Option>, Vec)> { + match encoded.split_first() { + Some((b's', mut rest)) => { + let stream = unescape_one(&mut rest)?; + Some((Some(stream), rest.to_vec())) + } + Some((b'g', rest)) => Some((None, rest.to_vec())), + _ => None, + } + } + + // these values are needed to keep the order preserved + const ESCAPE: u8 = 1; + const SEPARATOR: u8 = 0; + + /// Escape into an existing vec. + fn escape_into(components: I, result: &mut Vec) + where + I: IntoIterator, + C: AsRef<[u8]>, + { + for segment in components.into_iter() { + for &byte in segment.as_ref() { + match byte { + ESCAPE => result.extend([ESCAPE, ESCAPE]), + SEPARATOR => result.extend([ESCAPE, SEPARATOR]), + _ => result.push(byte), + } + } + result.push(SEPARATOR); + } + // you might think that the trailing separator is unnecessary, but it is needed + // to distinguish between the empty path and the path with one empty component + } + + fn unescape_one(path: &mut &[u8]) -> Option> { + let mut segment = Vec::new(); + let mut escape = false; + for (i, &byte) in path.iter().enumerate() { + if escape { + segment.push(byte); + escape = false; + } else { + match byte { + ESCAPE => escape = true, + SEPARATOR => { + *path = &path[i + 1..]; + return Some(segment); + } + _ => segment.push(byte), + } + } + } + None + } + + /// A simple version of unescape. + #[allow(dead_code)] + fn unescape(path: &[u8]) -> Vec> { + let mut components = Vec::new(); + let mut segment = Vec::new(); + let mut escape = false; + for &byte in path { + if escape { + segment.push(byte); + escape = false; + } else { + match byte { + ESCAPE => escape = true, + SEPARATOR => { + components.push(segment); + segment = Vec::new(); + } + _ => segment.push(byte), + } + } + } + components + } + + pub fn format_bytes(bytes: &[u8]) -> String { + if bytes.is_empty() { + return "\"\"".to_string(); + } + let Ok(s) = std::str::from_utf8(bytes) else { + return hex::encode(bytes); + }; + if s.chars() + .any(|c| c.is_control() && c != '\n' && c != '\t' && c != '\r') + { + return hex::encode(bytes); + } + format!("\"{}\"", escape_string(s)) + } + + pub fn escape_string(s: &str) -> String { + s.chars() + .map(|c| match c { + '"' => "\\\"".to_string(), + '\\' => "\\\\".to_string(), + '\n' => "\\n".to_string(), + '\t' => "\\t".to_string(), + '\r' => "\\r".to_string(), + c => c.to_string(), + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::db::util; + + #[test] + fn escape_unescape() { + let cases: Vec<(Option<&[u8]>, &[u8])> = vec![ + (None, b"key1"), + (Some(b""), b""), + (Some(b""), b"a"), + (Some(b"a"), b""), + ]; + for (stream, key) in cases { + let encoded = util::encode_stream_and_key(stream, key); + let (decoded_stream, decoded_key) = util::decode_stream_and_key(&encoded).unwrap(); + assert_eq!(decoded_stream.as_deref(), stream); + assert_eq!(decoded_key.as_slice(), key); + } + } +} diff --git a/rust/iroh-streamplace/src/node/db/kv/public_key.rs b/rust/iroh-streamplace/src/node/db/kv/public_key.rs new file mode 100644 index 00000000..5c5788d3 --- /dev/null +++ b/rust/iroh-streamplace/src/node/db/kv/public_key.rs @@ -0,0 +1,102 @@ +use std::{fmt, str::FromStr}; + +use snafu::Snafu; + +#[derive(Debug, Snafu, uniffi::Error)] +#[snafu(module)] +pub enum PublicKeyError { + Length { size: u64 }, + Invalid { message: String }, +} + +/// A public key. +/// +/// The key itself is just a 32 byte array, but a key has associated crypto +/// information that is cached for performance reasons. +#[derive(Clone, Copy, Eq, Ord, PartialOrd, uniffi::Object)] +#[uniffi::export(Display)] +pub struct PublicKey { + pub(crate) key: [u8; 32], +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + iroh::PublicKey::from(self).fmt(f) + } +} + +impl From for PublicKey { + fn from(key: iroh::PublicKey) -> Self { + PublicKey { + key: *key.as_bytes(), + } + } +} + +impl From<&PublicKey> for iroh::PublicKey { + fn from(key: &PublicKey) -> Self { + iroh::PublicKey::from_bytes(&key.key).unwrap() + } +} + +#[uniffi::export] +impl PublicKey { + /// Returns true if the PublicKeys are equal + pub fn equal(&self, other: &PublicKey) -> bool { + *self == *other + } + + /// Express the PublicKey as a byte array + pub fn as_vec(&self) -> Vec { + self.key.to_vec() + } + + /// Make a PublicKey from base32 string + #[uniffi::constructor] + #[allow(clippy::result_large_err)] + pub fn from_string(s: String) -> Result { + if s.len() != 64 { + return Err(PublicKeyError::Length { + size: s.len() as u64, + }); + } + let key = iroh::PublicKey::from_str(&s).map_err(|e| PublicKeyError::Invalid { + message: e.to_string(), + })?; + Ok(key.into()) + } + + /// Make a PublicKey from byte array + #[uniffi::constructor] + #[allow(clippy::result_large_err)] + pub fn from_bytes(bytes: Vec) -> Result { + if bytes.len() != 32 { + return Err(PublicKeyError::Length { + size: bytes.len() as u64, + }); + } + let bytes: [u8; 32] = bytes.try_into().expect("checked above"); + let key = iroh::PublicKey::from_bytes(&bytes).map_err(|e| PublicKeyError::Invalid { + message: e.to_string(), + })?; + Ok(key.into()) + } + + /// Convert to a base32 string limited to the first 10 bytes for a friendly string + /// representation of the key. + pub fn fmt_short(&self) -> String { + iroh::PublicKey::from(self).fmt_short() + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &PublicKey) -> bool { + self.key == other.key + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + iroh::PublicKey::from(self).fmt(f) + } +} diff --git a/rust/iroh-streamplace/src/node/db/kv/subscribe_mode.rs b/rust/iroh-streamplace/src/node/db/kv/subscribe_mode.rs new file mode 100644 index 00000000..7d736eae --- /dev/null +++ b/rust/iroh-streamplace/src/node/db/kv/subscribe_mode.rs @@ -0,0 +1,29 @@ +use iroh_smol_kv as w; + +/// Subscription mode for key-value subscriptions. +#[derive(uniffi::Enum, Debug, Clone, Copy)] +pub enum SubscribeMode { + Current, + Future, + Both, +} + +impl From for SubscribeMode { + fn from(m: w::SubscribeMode) -> Self { + match m { + w::SubscribeMode::Current => SubscribeMode::Current, + w::SubscribeMode::Future => SubscribeMode::Future, + w::SubscribeMode::Both => SubscribeMode::Both, + } + } +} + +impl From for w::SubscribeMode { + fn from(m: SubscribeMode) -> Self { + match m { + SubscribeMode::Current => w::SubscribeMode::Current, + SubscribeMode::Future => w::SubscribeMode::Future, + SubscribeMode::Both => w::SubscribeMode::Both, + } + } +} diff --git a/rust/iroh-streamplace/src/node/db/kv/time_bound.rs b/rust/iroh-streamplace/src/node/db/kv/time_bound.rs new file mode 100644 index 00000000..60920da5 --- /dev/null +++ b/rust/iroh-streamplace/src/node/db/kv/time_bound.rs @@ -0,0 +1,29 @@ +use std::ops::Bound; + +/// A bound on time for filtering. +#[derive(uniffi::Enum, Debug, Clone, Copy)] +pub enum TimeBound { + Unbounded, + Included(u64), + Excluded(u64), +} + +impl From> for TimeBound { + fn from(b: Bound) -> Self { + match b { + Bound::Unbounded => TimeBound::Unbounded, + Bound::Included(t) => TimeBound::Included(t), + Bound::Excluded(t) => TimeBound::Excluded(t), + } + } +} + +impl From for Bound { + fn from(b: TimeBound) -> Self { + match b { + TimeBound::Unbounded => Bound::Unbounded, + TimeBound::Included(t) => Bound::Included(t), + TimeBound::Excluded(t) => Bound::Excluded(t), + } + } +} diff --git a/rust/iroh-streamplace/src/node/mod.rs b/rust/iroh-streamplace/src/node/mod.rs new file mode 100644 index 00000000..f625025a --- /dev/null +++ b/rust/iroh-streamplace/src/node/mod.rs @@ -0,0 +1,13 @@ +use std::sync::LazyLock; + +/// We export the entire API at top level since this is what go-uniffi-bindgen will do anyway. +mod streams; +pub use streams::*; +mod db; +pub use db::*; +#[cfg(test)] +mod tests; + +/// Lazily initialized Tokio runtime for use in uniffi methods that need a runtime. +static RUNTIME: LazyLock = + LazyLock::new(|| tokio::runtime::Runtime::new().unwrap()); diff --git a/rust/iroh-streamplace/src/node/streams.rs b/rust/iroh-streamplace/src/node/streams.rs new file mode 100644 index 00000000..b5e3bff2 --- /dev/null +++ b/rust/iroh-streamplace/src/node/streams.rs @@ -0,0 +1,734 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + str::FromStr, + sync::Arc, +}; + +use bytes::Bytes; +use iroh::{NodeId, PublicKey, RelayMode, SecretKey, Watcher}; +use iroh_base::ticket::NodeTicket; +use iroh_gossip::{net::Gossip, proto::TopicId}; +use irpc::{WithChannels, rpc::RemoteService}; +use irpc_iroh::{IrohProtocol, IrohRemoteConnection}; +use n0_future::future::Boxed; + +mod rpc { + //! Protocol API + use bytes::Bytes; + use iroh::NodeId; + use irpc::{channel::oneshot, rpc_requests}; + use serde::{Deserialize, Serialize}; + + pub const ALPN: &[u8] = b"/iroh/streamplace/1"; + + /// Subscribe to the given `key` + #[derive(Debug, Serialize, Deserialize)] + pub struct Subscribe { + pub key: String, + // TODO: verify + pub remote_id: NodeId, + } + + /// Unsubscribe from the given `key` + #[derive(Debug, Serialize, Deserialize)] + pub struct Unsubscribe { + pub key: String, + // TODO: verify + pub remote_id: NodeId, + } + + // #[derive(Debug, Serialize, Deserialize)] + // pub struct SendSegment { + // pub key: String, + // pub data: Bytes, + // } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RecvSegment { + pub key: String, + pub data: Bytes, + } + + // Use the macro to generate both the Protocol and Message enums + // plus implement Channels for each type + #[rpc_requests(message = Message)] + #[derive(Serialize, Deserialize, Debug)] + pub enum Protocol { + #[rpc(tx=oneshot::Sender<()>)] + Subscribe(Subscribe), + #[rpc(tx=oneshot::Sender<()>)] + Unsubscribe(Unsubscribe), + #[rpc(tx=oneshot::Sender<()>)] + RecvSegment(RecvSegment), + } +} + +mod api { + //! Protocol API + use bytes::Bytes; + use iroh::{NodeAddr, NodeId}; + use irpc::{channel::oneshot, rpc_requests}; + use serde::{Deserialize, Serialize}; + + /// Subscribe to the given `key` + #[derive(Debug, Serialize, Deserialize)] + pub struct Subscribe { + pub key: String, + // TODO: verify + pub remote_id: NodeId, + } + + /// Unsubscribe from the given `key` + #[derive(Debug, Serialize, Deserialize)] + pub struct Unsubscribe { + pub key: String, + // TODO: verify + pub remote_id: NodeId, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct SendSegment { + pub key: String, + pub data: Bytes, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct JoinPeers { + pub peers: Vec, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct GetNodeAddr; + + // Use the macro to generate both the Protocol and Message enums + // plus implement Channels for each type + #[rpc_requests(message = Message)] + #[derive(Serialize, Deserialize, Debug)] + pub enum Protocol { + #[rpc(tx=oneshot::Sender<()>)] + Subscribe(Subscribe), + #[rpc(tx=oneshot::Sender<()>)] + Unsubscribe(Unsubscribe), + #[rpc(tx=oneshot::Sender<()>)] + SendSegment(SendSegment), + #[rpc(tx=oneshot::Sender<()>)] + JoinPeers(JoinPeers), + #[rpc(tx=oneshot::Sender)] + GetNodeAddr(GetNodeAddr), + } +} +use api::{Message as ApiMessage, Protocol as ApiProtocol}; +use n0_future::{FuturesUnordered, StreamExt}; +use rpc::{Message as RpcMessage, Protocol as RpcProtocol}; +use snafu::Snafu; +use tracing::{Instrument, debug, error, trace, trace_span, warn}; + +use super::{Config, CreateError, JoinPeersError, PutError, db, streams::rpc::RecvSegment}; + +pub(crate) enum HandlerMode { + Sender, + Forwarder, + Receiver(Box) -> Boxed<()> + Send + Sync + 'static>), +} + +impl HandlerMode { + pub fn receiver_fn(f: F) -> Self + where + F: Fn(String, Vec) -> Fut + Send + Sync + 'static, + Fut: std::future::Future + Send + 'static, + { + Self::Receiver(Box::new(move |name, data| Box::pin(f(name, data)))) + } + + pub fn receiver(handler: Arc) -> Self { + Self::receiver_fn(move |id, data| { + let handler = handler.clone(); + async move { + handler.handle_data(id, data).await; + } + }) + } + + pub fn mode_str(&self) -> &'static str { + match self { + HandlerMode::Sender => "sender", + HandlerMode::Forwarder => "forwarder", + HandlerMode::Receiver(_) => "receiver", + } + } +} + +type Tasks = FuturesUnordered)>>; + +/// Actor that contains both a kv db for metadata and a handler for the rpc protocol. +/// +/// This can be used both for sender and receiver nodes. Sender nodes will just set the +/// handler to None. +struct Actor { + /// Receiver for rpc messages from remote nodes + rpc_rx: tokio::sync::mpsc::Receiver, + /// Receiver for API messages from the user + api_rx: tokio::sync::mpsc::Receiver, + /// nodes I need to send to for each stream + subscribers: BTreeMap>, + /// nodes I am subscribed to + subscriptions: BTreeMap, + /// lightweight typed connection pool + connections: ConnectionPool, + /// How to handle incoming data + handler: HandlerMode, + /// Iroh protocol router, I need to keep it around to keep the protocol alive + router: iroh::protocol::Router, + /// Metadata db + client: db::Db, + /// Write scope for this node for the metadata db + write: db::WriteScope, + /// Ongoing tasks + tasks: Tasks, + /// Configuration, needed for timeouts etc. + config: Arc, +} + +#[derive(Debug, Clone)] +struct Connection { + id: NodeId, + rpc: irpc::Client, +} + +#[derive(Debug, Snafu)] +enum RpcTaskError { + #[snafu(transparent)] + Task { source: irpc::Error }, + #[snafu(transparent)] + Timeout { source: tokio::time::error::Elapsed }, +} + +struct ConnectionPool { + endpoint: iroh::Endpoint, + connections: BTreeMap, +} + +impl ConnectionPool { + fn new(endpoint: iroh::Endpoint) -> Self { + Self { + endpoint, + connections: BTreeMap::new(), + } + } + + /// Cheap conn pool hack + fn get(&mut self, remote: &NodeId) -> Connection { + if !self.connections.contains_key(remote) { + let conn = IrohRemoteConnection::new( + self.endpoint.clone(), + (*remote).into(), + rpc::ALPN.to_vec(), + ); + let conn = Connection { + rpc: irpc::Client::boxed(conn), + id: *remote, + }; + self.connections.insert(*remote, conn); + } + self.connections + .get_mut(remote) + .expect("just inserted") + .clone() + } + + fn remove(&mut self, remote: &NodeId) { + self.connections.remove(remote); + } +} + +impl Actor { + pub async fn spawn( + endpoint: iroh::Endpoint, + topic: iroh_gossip::proto::TopicId, + config: super::Config, + handler: HandlerMode, + ) -> Result<(Node, impl Future), iroh_gossip::api::ApiError> { + let (rpc_tx, rpc_rx) = tokio::sync::mpsc::channel::(32); + let (api_tx, api_rx) = tokio::sync::mpsc::channel::(32); + let gossip = Gossip::builder().spawn(endpoint.clone()); + let id = endpoint.node_id(); + let router = iroh::protocol::Router::builder(endpoint.clone()) + .accept(iroh_gossip::ALPN, gossip.clone()) + .accept( + rpc::ALPN, + IrohProtocol::new(rpc::Protocol::remote_handler(rpc_tx.into())), + ) + .spawn(); + let topic = gossip.subscribe(topic, vec![]).await?; + let secret = router.endpoint().secret_key().clone(); + let db_config = Default::default(); + let client = iroh_smol_kv::Client::local(topic, db_config); + let write = db::WriteScope::new(client.write(secret.clone())); + let client = db::Db::new(client); + let actor = Self { + rpc_rx, + api_rx, + subscribers: BTreeMap::new(), + subscriptions: BTreeMap::new(), + connections: ConnectionPool::new(router.endpoint().clone()), + handler, + router, + write: write.clone(), + client: client.clone(), + tasks: FuturesUnordered::new(), + config: Arc::new(config), + }; + let api = Node { + client: Arc::new(client), + write: Arc::new(write), + api: irpc::Client::local(api_tx), + }; + Ok(( + api, + actor + .run() + .instrument(trace_span!("actor", id=%id.fmt_short())), + )) + } + + async fn run(mut self) { + loop { + tokio::select! { + msg = self.rpc_rx.recv() => { + let Some(msg) = msg else { + error!("rpc channel closed"); + break; + }; + self.handle_rpc(msg).instrument(trace_span!("rpc")).await; + } + msg = self.api_rx.recv() => { + let Some(msg) = msg else { + break; + }; + self.handle_api(msg).instrument(trace_span!("api")).await; + } + res = self.tasks.next(), if !self.tasks.is_empty() => { + let Some((remote_id, res)) = res else { + error!("task finished but no result"); + break; + }; + match res { + Ok(()) => {} + Err(RpcTaskError::Timeout { source }) => { + warn!("call to {remote_id} timed out: {source}"); + } + Err(RpcTaskError::Task { source }) => { + warn!("call to {remote_id} failed: {source}"); + } + } + self.connections.remove(&remote_id); + } + } + } + } + + async fn update_subscriber_meta(&mut self, key: &str) { + let n = self + .subscribers + .get(key) + .map(|s| s.len()) + .unwrap_or_default(); + let v = n.to_string().into_bytes(); + self.write + .put_impl(Some(key.as_bytes().to_vec()), b"subscribers", v.into()) + .await + .ok(); + } + + /// Requests from remote nodes + async fn handle_rpc(&mut self, msg: RpcMessage) { + match msg { + RpcMessage::Subscribe(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: rpc::Subscribe { key, remote_id }, + .. + } = msg; + self.subscribers + .entry(key.clone()) + .or_default() + .insert(remote_id); + self.update_subscriber_meta(&key).await; + tx.send(()).await.ok(); + } + RpcMessage::Unsubscribe(msg) => { + debug!("{:?}", msg.inner); + let WithChannels { + tx, + inner: rpc::Unsubscribe { key, remote_id }, + .. + } = msg; + if let Some(e) = self.subscribers.get_mut(&key) + && !e.remove(&remote_id) + { + warn!( + "unsubscribe: no subscription for {} from {}", + key, remote_id + ); + } + if let Some(subscriptions) = self.subscribers.get(&key) + && subscriptions.is_empty() + { + self.subscribers.remove(&key); + } + self.update_subscriber_meta(&key).await; + tx.send(()).await.ok(); + } + RpcMessage::RecvSegment(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: rpc::RecvSegment { key, data }, + .. + } = msg; + match &self.handler { + HandlerMode::Sender => { + warn!("received segment but in sender mode"); + } + HandlerMode::Forwarder => { + if let Some(remotes) = self.subscribers.get(&key) { + Self::handle_send( + &mut self.tasks, + &mut self.connections, + &self.config, + key, + data, + remotes, + ); + } else { + trace!("no subscribers for stream {}", key); + } + } + HandlerMode::Receiver(handler) => { + if self.subscriptions.contains_key(&key) { + handler(key, data.to_vec()).await; + } else { + warn!("received segment for unsubscribed key: {}", key); + } + } + }; + tx.send(()).await.ok(); + } + } + } + + async fn handle_api(&mut self, msg: ApiMessage) { + match msg { + ApiMessage::SendSegment(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: api::SendSegment { key, data }, + .. + } = msg; + if let Some(remotes) = self.subscribers.get(&key) { + Self::handle_send( + &mut self.tasks, + &mut self.connections, + &self.config, + key, + data, + remotes, + ); + } else { + trace!("no subscribers for stream {}", key); + } + tx.send(()).await.ok(); + } + ApiMessage::Subscribe(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: api::Subscribe { key, remote_id }, + .. + } = msg; + let conn = self.connections.get(&remote_id); + conn.rpc + .rpc(rpc::Subscribe { + key: key.clone(), + remote_id: self.node_id(), + }) + .await + .ok(); + self.subscriptions.insert(key, remote_id); + tx.send(()).await.ok(); + } + ApiMessage::Unsubscribe(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: api::Unsubscribe { key, remote_id }, + .. + } = msg; + let conn = self.connections.get(&remote_id); + conn.rpc + .rpc(rpc::Unsubscribe { + key: key.clone(), + remote_id: self.node_id(), + }) + .await + .ok(); + self.subscriptions.remove(&key); + tx.send(()).await.ok(); + } + ApiMessage::JoinPeers(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: api::JoinPeers { peers }, + .. + } = msg; + let ids = peers + .iter() + .map(|a| a.node_id) + .filter(|id| *id != self.node_id()) + .collect::>(); + for addr in &peers { + self.router.endpoint().add_node_addr(addr.clone()).ok(); + } + self.client.inner().join_peers(ids).await.ok(); + tx.send(()).await.ok(); + } + ApiMessage::GetNodeAddr(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { tx, .. } = msg; + if !self.config.disable_relay { + // don't await home relay if we have disabled relays, this will hang forever + self.router.endpoint().home_relay().initialized().await; + } + let addr = self.router.endpoint().node_addr().initialized().await; + tx.send(addr).await.ok(); + } + } + } + + fn handle_send( + tasks: &mut Tasks, + connections: &mut ConnectionPool, + config: &Arc, + key: String, + data: Bytes, + remotes: &BTreeSet, + ) { + let msg = rpc::RecvSegment { key, data }; + for remote in remotes { + trace!("sending to stream {}: {}", msg.key, remote); + let conn = connections.get(remote); + tasks.push(Box::pin(Self::forward_task( + config.clone(), + conn, + msg.clone(), + ))); + } + } + + async fn forward_task( + config: Arc, + conn: Connection, + msg: RecvSegment, + ) -> (NodeId, Result<(), RpcTaskError>) { + let id = conn.id; + let res = async move { + tokio::time::timeout(config.max_send_duration, conn.rpc.rpc(msg)).await??; + Ok(()) + } + .await; + (id, res) + } + + fn node_id(&self) -> PublicKey { + self.router.endpoint().node_id() + } +} + +/// Iroh-streamplace node that can send, forward or receive stream segments. +#[derive(Clone, uniffi::Object)] +pub struct Node { + client: Arc, + write: Arc, + api: irpc::Client, +} + +impl Node { + pub(crate) async fn new_in_runtime( + config: super::Config, + handler: HandlerMode, + ) -> Result, CreateError> { + let mode_str = Bytes::from(handler.mode_str()); + let secret_key = + SecretKey::from_bytes(&<[u8; 32]>::try_from(config.key.clone()).map_err(|e| { + CreateError::PrivateKey { + size: e.len() as u64, + } + })?); + let topic = + TopicId::from_bytes(<[u8; 32]>::try_from(config.topic.clone()).map_err(|e| { + CreateError::Topic { + size: e.len() as u64, + } + })?); + let relay_mode = if config.disable_relay { + RelayMode::Disabled + } else { + RelayMode::Default + }; + let endpoint = iroh::Endpoint::builder() + .secret_key(secret_key) + .relay_mode(relay_mode) + .bind() + .await + .map_err(|e| CreateError::Bind { + message: e.to_string(), + })?; + let (api, actor) = Actor::spawn(endpoint, topic, config, handler) + .await + .map_err(|e| CreateError::Subscribe { + message: e.to_string(), + })?; + api.node_scope() + .put_impl(Option::>::None, b"mode", mode_str) + .await + .ok(); + tokio::spawn(actor); + Ok(Arc::new(api)) + } +} + +/// DataHandler trait that is exported to go for receiving data callbacks. +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait DataHandler: Send + Sync { + async fn handle_data(&self, topic: String, data: Vec); +} + +#[uniffi::export] +impl Node { + /// Create a new streamplace client node. + #[uniffi::constructor] + pub async fn sender(config: super::Config) -> Result, CreateError> { + super::RUNTIME.block_on(Self::new_in_runtime(config, HandlerMode::Sender)) + } + + #[uniffi::constructor] + pub async fn forwarder(config: super::Config) -> Result, CreateError> { + super::RUNTIME.block_on(Self::new_in_runtime(config, HandlerMode::Forwarder)) + } + + #[uniffi::constructor] + pub async fn receiver( + config: super::Config, + handler: Arc, + ) -> Result, CreateError> { + super::RUNTIME.block_on(Self::new_in_runtime(config, HandlerMode::receiver(handler))) + } + + /// Get a handle to the db to watch for changes locally or globally. + pub fn db(&self) -> Arc { + self.client.clone() + } + + /// Get a handle to the write scope for this node. + /// + /// This is equivalent to calling `db.write(...)` with the secret key used to create the node. + pub fn node_scope(&self) -> Arc { + self.write.clone() + } + + /// Subscribe to updates for a given stream from a remote node. + pub async fn subscribe( + &self, + key: String, + remote_id: Arc, + ) -> Result<(), PutError> { + self.api + .rpc(api::Subscribe { + key, + remote_id: remote_id.as_ref().into(), + }) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + }) + } + + /// Unsubscribe from updates for a given stream from a remote node. + pub async fn unsubscribe( + &self, + key: String, + remote_id: Arc, + ) -> Result<(), PutError> { + self.api + .rpc(api::Unsubscribe { + key, + remote_id: remote_id.as_ref().into(), + }) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + }) + } + + /// Send a segment to all subscribers of the given stream. + pub async fn send_segment(&self, key: String, data: Vec) -> Result<(), PutError> { + self.api + .rpc(api::SendSegment { + key, + data: data.into(), + }) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + }) + } + + /// Join peers by their node tickets. + pub async fn join_peers(&self, peers: Vec) -> Result<(), JoinPeersError> { + let peers = peers + .iter() + .map(|p| NodeTicket::from_str(p)) + .collect::, _>>() + .map_err(|e| JoinPeersError::Ticket { + message: e.to_string(), + })?; + let addrs = peers + .iter() + .map(|t| t.node_addr().clone()) + .collect::>(); + self.api + .rpc(api::JoinPeers { peers: addrs }) + .await + .map_err(|e| JoinPeersError::Irpc { + message: e.to_string(), + }) + } + + /// Get this node's ticket. + pub async fn ticket(&self) -> Result { + let addr = self + .api + .rpc(api::GetNodeAddr) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + })?; + Ok(NodeTicket::from(addr).to_string()) + } + + /// Get this node's node ID. + pub async fn node_id(&self) -> Result, PutError> { + let addr = self + .api + .rpc(api::GetNodeAddr) + .await + .map_err(|e| PutError::Irpc { + message: e.to_string(), + })?; + Ok(Arc::new(addr.node_id.into())) + } +} diff --git a/rust/iroh-streamplace/src/node/tests.rs b/rust/iroh-streamplace/src/node/tests.rs new file mode 100644 index 00000000..a08e7202 --- /dev/null +++ b/rust/iroh-streamplace/src/node/tests.rs @@ -0,0 +1,299 @@ +use std::{ops::Deref, sync::Arc, time::Duration}; + +use async_trait::async_trait; +use n0_future::{BufferedStreamExt, StreamExt, stream}; +use testresult::TestResult; + +use super::{streams::HandlerMode, *}; + +struct TestNode { + node: Arc, + public: Arc, + ticket: String, + #[allow(dead_code)] + private: Vec, +} + +impl Deref for TestNode { + type Target = Node; + + fn deref(&self) -> &Self::Target { + &self.node + } +} + +impl TestNode { + /// Helper to create a test node with given config and handler mode. + async fn new(handler: HandlerMode) -> TestResult { + let config = Config { + key: vec![0_u8; 32], // will be replaced + topic: vec![0_u8; 32], // all nodes use the same topic + max_send_duration: Duration::from_secs(10), + disable_relay: false, + }; + Self::new_with_config(handler, config).await + } + + async fn new_with_config(handler: HandlerMode, mut config: Config) -> TestResult { + let key = iroh::SecretKey::generate(&mut rand::rngs::OsRng); + let key = key.to_bytes().to_vec(); + config.key = key.clone(); + let node = Node::new_in_runtime(config, handler).await?; + let public = node.node_id().await?; + let ticket = node.ticket().await?; + Ok(TestNode { + node, + private: key, + public, + ticket, + }) + } +} + +/// Helper to create multiple test nodes with given handler mode. +async fn test_nodes( + n: usize, + handler: impl Fn(usize) -> HandlerMode + Send + Sync, + disable_relay: bool, +) -> TestResult> { + const PAR: usize = 32; + let modes = (0..n).map(handler).collect::>(); + + let config = Config { + key: vec![0_u8; 32], // will be replaced + topic: vec![0_u8; 32], + max_send_duration: Duration::from_secs(10), + disable_relay, + }; + // create all nodes in parallel + let nodes = stream::iter(modes) + .map(|mode| TestNode::new_with_config(mode, config.clone())) + .buffered_unordered(PAR) + .collect::>() + .await; + let nodes = nodes.into_iter().collect::>>()?; + // join everyone to everyone + let tickets = nodes.iter().map(|n| n.ticket.clone()).collect::>(); + let res = stream::iter(&nodes) + .map(|n| n.join_peers(tickets.clone())) + .buffered_unordered(PAR) + .collect::>() + .await; + res.into_iter().collect::, _>>()?; + Ok(nodes) +} + +#[tokio::test] +async fn one_node() -> TestResult<()> { + tracing_subscriber::fmt::try_init().ok(); + let node = TestNode::new(HandlerMode::Sender).await?.node; + let write = node.node_scope(); + let db = node.db(); + println!("Ticket: {}", node.ticket().await?); + write + .put(Some(b"stream1".to_vec()), b"s".to_vec(), b"y".to_vec()) + .await?; + write + .put(Some(b"stream2".to_vec()), b"s".to_vec(), b"y".to_vec()) + .await?; + let res = db.subscribe_with_opts(SubscribeOpts { + filter: Filter::new(), + mode: SubscribeMode::Both, + }); + while let Some(item) = res.next_raw().await? { + if let SubscribeItem::CurrentDone = item { + break; + } + println!("Got item: {item:?}"); + } + let res = db + .iter_with_opts( + Filter::new() + .stream(b"stream1".to_vec()) + .scope(node.node_id().await?), + ) + .await?; + println!("Iter result: {res:?}"); + Ok(()) +} + +struct TestHandler { + info: T, + sender: tokio::sync::mpsc::Sender<(T, String, Vec)>, +} + +impl TestHandler { + fn new(info: T, sender: tokio::sync::mpsc::Sender<(T, String, Vec)>) -> Self { + Self { info, sender } + } +} + +#[async_trait] +impl DataHandler for TestHandler { + async fn handle_data(&self, topic: String, data: Vec) { + self.sender + .send((self.info.clone(), topic, data)) + .await + .ok(); + } +} + +#[tokio::test] +async fn two_nodes_send_receive() -> TestResult<()> { + tracing_subscriber::fmt::try_init().ok(); + let (tx, mut rx) = tokio::sync::mpsc::channel(32); + let handler = Arc::new(TestHandler::new((), tx)); + let sender = TestNode::new(HandlerMode::Sender).await?; + let receiver = TestNode::new(HandlerMode::receiver(handler)).await?; + // join the sender to the receiver. This will also configure the receiver endpoint to be able to dial the sender. + receiver.join_peers(vec![sender.ticket.clone()]).await?; + let stream = "teststream".to_string(); + receiver + .subscribe(stream.clone(), sender.public.clone()) + .await?; + sender.send_segment(stream, b"segment1".to_vec()).await?; + let (_, stream, data) = rx.recv().await.expect("should get data"); + assert_eq!(stream, "teststream"); + assert_eq!(data, b"segment1".to_vec()); + Ok(()) +} + +#[tokio::test] +async fn three_nodes_send_forward_receive() -> TestResult<()> { + tracing_subscriber::fmt::try_init().ok(); + let (tx, mut rx) = tokio::sync::mpsc::channel(32); + let handler = Arc::new(TestHandler::new((), tx)); + let sender = TestNode::new(HandlerMode::Sender).await?; + let forwarder = TestNode::new(HandlerMode::Forwarder).await?; + let receiver = TestNode::new(HandlerMode::receiver(handler)).await?; + // join everyone to everyone, so the receiver can reach the sender via the forwarder. + let tickets = vec![ + sender.ticket.clone(), + forwarder.ticket.clone(), + receiver.ticket.clone(), + ]; + receiver.join_peers(tickets.clone()).await?; + forwarder.join_peers(tickets.clone()).await?; + sender.join_peers(tickets).await?; + let stream = "teststream".to_string(); + receiver + .subscribe(stream.clone(), forwarder.public.clone()) + .await?; + forwarder + .subscribe(stream.clone(), sender.public.clone()) + .await?; + sender.send_segment(stream, b"segment1".to_vec()).await?; + let (_, stream, data) = rx.recv().await.expect("should get data"); + assert_eq!(stream, "teststream"); + assert_eq!(data, b"segment1".to_vec()); + Ok(()) +} + +#[tokio::test] +async fn meta_three_nodes_send_forward_receive() -> TestResult<()> { + tracing_subscriber::fmt::try_init().ok(); + let (tx, mut rx) = tokio::sync::mpsc::channel(32); + let handler = Arc::new(TestHandler::new((), tx)); + let sender = TestNode::new(HandlerMode::Sender).await?; + let forwarder = TestNode::new(HandlerMode::Forwarder).await?; + let receiver = TestNode::new(HandlerMode::receiver(handler)).await?; + // join everyone to everyone, so the receiver can reach the sender via the forwarder. + let tickets = vec![ + sender.ticket.clone(), + forwarder.ticket.clone(), + receiver.ticket.clone(), + ]; + receiver.join_peers(tickets.clone()).await?; + forwarder.join_peers(tickets.clone()).await?; + sender.join_peers(tickets).await?; + let stream = "teststream".to_string(); + receiver + .subscribe(stream.clone(), forwarder.public.clone()) + .await?; + forwarder + .subscribe(stream.clone(), sender.public.clone()) + .await?; + sender.send_segment(stream, b"segment1".to_vec()).await?; + let (_, stream, data) = rx.recv().await.expect("should get data"); + assert_eq!(stream, "teststream"); + assert_eq!(data, b"segment1".to_vec()); + let stream = receiver.db().subscribe(Filter::new()); + while let Some(item) = stream.next_raw().await? { + println!("{}", subscribe_item_debug(&item)); + } + Ok(()) +} + +async fn broadcast( + nsenders: usize, + nforwarders: usize, + nreceivers: usize, + nmsgs: usize, +) -> TestResult<()> { + let (tx, mut rx) = tokio::sync::mpsc::channel(32); + let ntotal = nsenders + nforwarders + nreceivers; + let senders = 0..nsenders; + let forwarders = nsenders..(nsenders + nforwarders); + let receivers = (nsenders + nforwarders)..ntotal; + let make_handler = |i: usize| { + if senders.contains(&i) { + HandlerMode::Sender + } else if forwarders.contains(&i) { + HandlerMode::Forwarder + } else { + HandlerMode::receiver(Arc::new(TestHandler::new(i, tx.clone()))) + } + }; + let nodes = test_nodes(ntotal, make_handler, true).await?; + let senders = &nodes[senders]; + let forwarders = &nodes[forwarders]; + let receivers = &nodes[receivers]; + let stream = "teststream".to_string(); + // subscribe all forwarders to a sender, round robin + for (i, forwarder) in forwarders.iter().enumerate() { + let sender = &senders[i % senders.len()]; + forwarder + .subscribe(stream.clone(), sender.public.clone()) + .await?; + } + // subscribe all receivers to a forwarder, round robin + for (i, receiver) in receivers.iter().enumerate() { + let forwarder = &forwarders[i % forwarders.len()]; + receiver + .subscribe(stream.clone(), forwarder.public.clone()) + .await?; + } + for _ in 0..nmsgs { + for sender in senders { + sender + .send_segment(stream.clone(), b"segment1".to_vec()) + .await?; + } + for _ in 0..receivers.len() { + let (i, stream, _) = rx.recv().await.expect("should get data"); + println!("Node {i} got data on stream {stream}"); + } + } + Ok(()) +} + +#[tokio::test] +async fn broadcast_1_2_4() -> TestResult<()> { + tracing_subscriber::fmt().try_init().ok(); + broadcast(1, 2, 4, 1).await?; + Ok(()) +} + +#[tokio::test] +async fn broadcast_1_3_9() -> TestResult<()> { + tracing_subscriber::fmt().try_init().ok(); + broadcast(1, 3, 9, 1).await?; + Ok(()) +} + +#[tokio::test] +async fn broadcast_1_4_16() -> TestResult<()> { + tracing_subscriber::fmt().try_init().ok(); + broadcast(1, 4, 16, 100).await?; + Ok(()) +} diff --git a/rust/iroh-streamplace/src/receiver.rs b/rust/iroh-streamplace/src/receiver.rs index 61743d73..9a995a57 100644 --- a/rust/iroh-streamplace/src/receiver.rs +++ b/rust/iroh-streamplace/src/receiver.rs @@ -2,11 +2,7 @@ use std::sync::Arc; use iroh::protocol::Router; -use crate::api::Api; -use crate::endpoint::Endpoint; -use crate::error::Error; -use crate::key::PublicKey; -use crate::utils::NodeAddr; +use crate::{api::Api, endpoint::Endpoint, error::Error, key::PublicKeyOld, utils::NodeAddr}; #[derive(uniffi::Object)] pub struct Receiver { @@ -21,7 +17,7 @@ impl Receiver { #[uniffi::constructor(async_runtime = "tokio")] pub async fn new( endpoint: &Endpoint, - handler: Arc, + handler: Arc, ) -> Result { let api = Api::spawn_with_handler(&endpoint.endpoint, move |id, data| { let handler = handler.clone(); @@ -42,7 +38,7 @@ impl Receiver { /// Subscribe to the given topic on the remote. #[uniffi::method(async_runtime = "tokio")] - pub async fn subscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { + pub async fn subscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { let remote_id: iroh::NodeId = remote_id.as_ref().into(); let api = Api::connect(self.endpoint.endpoint.clone(), remote_id); api.subscribe(topic.to_string(), self.endpoint.endpoint.node_id()) @@ -52,7 +48,11 @@ impl Receiver { /// Unsubscribe from this topic on the remote. #[uniffi::method(async_runtime = "tokio")] - pub async fn unsubscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { + pub async fn unsubscribe( + &self, + remote_id: Arc, + topic: &str, + ) -> Result<(), Error> { let remote_id: iroh::NodeId = remote_id.as_ref().into(); let api = Api::connect(self.endpoint.endpoint.clone(), remote_id); api.unsubscribe(topic.to_string(), self.endpoint.endpoint.node_id()) @@ -68,16 +68,15 @@ impl Receiver { #[uniffi::export(with_foreign)] #[async_trait::async_trait] -pub trait DataHandler: Send + Sync { +pub trait DataHandlerOld: Send + Sync { async fn handle_data(&self, topic: String, data: Vec); } #[cfg(test)] mod tests { - use crate::sender::Sender; - use super::*; + use crate::sender::Sender; #[tokio::test] async fn test_roundtrip() { @@ -96,7 +95,7 @@ mod tests { } #[async_trait::async_trait] - impl DataHandler for TestHandler { + impl DataHandlerOld for TestHandler { async fn handle_data(&self, topic: String, data: Vec) { self.messages.send((topic, data)).await.unwrap(); } @@ -109,10 +108,10 @@ mod tests { .unwrap(); let sender_addr = sender.node_addr().await; - println!("sender addr: {:?}", sender_addr); + println!("sender addr: {sender_addr:?}"); let receiver_addr = receiver.node_addr().await; - println!("recv addr: {:?}", receiver_addr); + println!("recv addr: {receiver_addr:?}"); // subscribe receiver diff --git a/rust/iroh-streamplace/src/sender.rs b/rust/iroh-streamplace/src/sender.rs index ab3b8d86..99ec5ae6 100644 --- a/rust/iroh-streamplace/src/sender.rs +++ b/rust/iroh-streamplace/src/sender.rs @@ -1,10 +1,7 @@ use bytes::Bytes; use iroh::protocol::Router; -use crate::api::Api; -use crate::endpoint::Endpoint; -use crate::error::Error; -use crate::utils::NodeAddr; +use crate::{api::Api, endpoint::Endpoint, error::Error, utils::NodeAddr}; #[derive(uniffi::Object)] pub struct Sender { diff --git a/rust/iroh-streamplace/src/utils.rs b/rust/iroh-streamplace/src/utils.rs index 2a95b731..cc02d198 100644 --- a/rust/iroh-streamplace/src/utils.rs +++ b/rust/iroh-streamplace/src/utils.rs @@ -1,13 +1,11 @@ -use std::str::FromStr; -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; -use crate::error::Error; -use crate::key::PublicKey; +use crate::{error::Error, key::PublicKeyOld}; /// A peer and it's addressing information. #[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] pub struct NodeAddr { - node_id: Arc, + node_id: Arc, relay_url: Option, addresses: Vec, } @@ -16,7 +14,7 @@ pub struct NodeAddr { impl NodeAddr { /// Create a new [`NodeAddr`] with empty [`AddrInfo`]. #[uniffi::constructor] - pub fn new(node_id: &PublicKey, derp_url: Option, addresses: Vec) -> Self { + pub fn new(node_id: &PublicKeyOld, derp_url: Option, addresses: Vec) -> Self { Self { node_id: Arc::new(node_id.clone()), relay_url: derp_url, @@ -24,7 +22,7 @@ impl NodeAddr { } } - pub fn node_id(&self) -> PublicKey { + pub fn node_id(&self) -> PublicKeyOld { self.node_id.as_ref().clone() } -- 2.51.2 From 52db4067c9a9414f9db5af06d54afb37d75f6b3a Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 30 Sep 2025 16:48:23 +0300 Subject: [PATCH 075/525] Add go test code --- rust/iroh-streamplace/.gitignore | 3 +- rust/iroh-streamplace/Makefile | 19 +++++++ rust/iroh-streamplace/go/go.mod | 3 + rust/iroh-streamplace/go/main.go | 95 ++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 rust/iroh-streamplace/Makefile create mode 100644 rust/iroh-streamplace/go/go.mod create mode 100644 rust/iroh-streamplace/go/main.go diff --git a/rust/iroh-streamplace/.gitignore b/rust/iroh-streamplace/.gitignore index ea8c4bf7..526ee3b6 100644 --- a/rust/iroh-streamplace/.gitignore +++ b/rust/iroh-streamplace/.gitignore @@ -1 +1,2 @@ -/target +/go/iroh_streamplace/* +/go/*.dylib diff --git a/rust/iroh-streamplace/Makefile b/rust/iroh-streamplace/Makefile new file mode 100644 index 00000000..cf684ec0 --- /dev/null +++ b/rust/iroh-streamplace/Makefile @@ -0,0 +1,19 @@ +.PHONY: clean build-rust generate run + +install: + cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.4.0+v0.28.3 + +clean: + cargo clean + rm -rf go/uniffi_example/ + +build-rust: + cargo build + +generate: build-rust + uniffi-bindgen-go --library ../../target/debug/libiroh_streamplace.dylib --out-dir go/ + cp ../../target/debug/libiroh_streamplace.dylib go/ + +run: generate + cd go && CGO_LDFLAGS="-L. -liroh_streamplace" go run main.go $(TICKET) + diff --git a/rust/iroh-streamplace/go/go.mod b/rust/iroh-streamplace/go/go.mod new file mode 100644 index 00000000..4a93f614 --- /dev/null +++ b/rust/iroh-streamplace/go/go.mod @@ -0,0 +1,3 @@ +module hello-app + +go 1.25.1 diff --git a/rust/iroh-streamplace/go/main.go b/rust/iroh-streamplace/go/main.go new file mode 100644 index 00000000..70e2ec3c --- /dev/null +++ b/rust/iroh-streamplace/go/main.go @@ -0,0 +1,95 @@ + +package main + +import ( + "fmt" + "os" + sp "hello-app/iroh_streamplace" + "reflect" + "crypto/rand" +) + +func isNilError(err error) bool { + if err == nil { + return true + } + + v := reflect.ValueOf(err) + return v.Kind() == reflect.Ptr && v.IsNil() +} + + +func panicIfErr(err error) { + if !isNilError(err) { + panic(err) + } +} + +func main() { + tickets := os.Args[1:]; + + secret := make([]byte, 32) + _, err := rand.Read(secret) + panicIfErr(err) + + fmt.Println("Starting with tickets", tickets) + config := sp.Config { + Key : secret, + Topic: make([]byte, 32), // all zero topic for testing + MaxSendDuration: 1000_000_000, // 1s + } + fmt.Printf("Config created %+v\n", config) + node, err := sp.NodeSender(config) + panicIfErr(err) + + db := node.Db() + w := node.NodeScope() + + node_id, err := node.NodeId() + panicIfErr(err) + fmt.Println("Node ID:", node_id) + + ticket, err := node.Ticket() + panicIfErr(err) + fmt.Println("Ticket:", ticket) + + if len(tickets) > 0 { + err = node.JoinPeers(tickets) + panicIfErr(err) + } + + w.Put(nil, []byte("hello"), []byte("world")) + stream := []byte("stream1") + w.Put(&stream, []byte("subscribed"), []byte("true")) + + filter := sp.NewFilter() + items, err := db.IterWithOpts(filter) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items) + + filter2 := sp.NewFilter().Global() + items2, err := db.IterWithOpts(filter2) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items2) + + filter3 := sp.NewFilter().Stream(stream) + items3, err := db.IterWithOpts(filter3) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items3) + + sub := db.Subscribe(sp.NewFilter()) + for { + ev, err := sub.NextRaw() + panicIfErr(err) + switch (*ev).(type) { + case sp.SubscribeItemEntry: + fmt.Printf("%+v\n", (*ev).(sp.SubscribeItemEntry)) + case sp.SubscribeItemCurrentDone: + fmt.Printf("Got current done event: %+v\n", (*ev).(sp.SubscribeItemCurrentDone)) + case sp.SubscribeItemExpired: + fmt.Printf("Got expired event: %+v\n", (*ev).(sp.SubscribeItemExpired)) + case sp.SubscribeItemOther: + fmt.Printf("Got other event: %+v\n", (*ev).(sp.SubscribeItemOther)) + } + } +} \ No newline at end of file -- 2.51.2 From 4a747a6bf649ea94af845c0fe09f96189265e3f9 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 30 Sep 2025 18:01:06 +0300 Subject: [PATCH 076/525] replace key with the new public_key make node_addr more self-contained (own error type) --- Makefile | 6 +- .../iroh_streamplace/iroh_streamplace.go | 4471 ++++++++++++++--- .../iroh_streamplace/iroh_streamplace.h | 478 +- rust/iroh-streamplace/src/endpoint.rs | 2 +- rust/iroh-streamplace/src/error.rs | 7 - rust/iroh-streamplace/src/key.rs | 108 - rust/iroh-streamplace/src/lib.rs | 4 +- rust/iroh-streamplace/src/node/db.rs | 6 +- rust/iroh-streamplace/src/node/streams.rs | 6 +- rust/iroh-streamplace/src/node/tests.rs | 2 +- .../src/{utils.rs => node_addr.rs} | 24 +- .../src/{node/db/kv => }/public_key.rs | 0 rust/iroh-streamplace/src/receiver.rs | 6 +- rust/iroh-streamplace/src/sender.rs | 2 +- 14 files changed, 4195 insertions(+), 927 deletions(-) delete mode 100644 rust/iroh-streamplace/src/key.rs rename rust/iroh-streamplace/src/{utils.rs => node_addr.rs} (72%) rename rust/iroh-streamplace/src/{node/db/kv => }/public_key.rs (100%) diff --git a/Makefile b/Makefile index 464cddd4..ecfa3706 100644 --- a/Makefile +++ b/Makefile @@ -291,8 +291,10 @@ dev-rust: EXT=so; \ if [ "$(BUILDOS)" = "darwin" ]; then EXT=dylib; fi; \ uniffi-bindgen-go --out-dir pkg/iroh/generated --library ./target/debug/libiroh_streamplace.$$EXT \ - && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/rust/iroh-streamplace/libiroh_streamplace.$$EXT \ - && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/lib/libiroh_streamplace.$$EXT + && mkdir -p $(BUILDDIR)/rust/iroh-streamplace/ \ + && mkdir -p $(BUILDDIR)/lib/ \ + && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/rust/iroh-streamplace/ \ + && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/lib/ .PHONY: dev-test dev-test: diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index 973746a7..a4c5a64b 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -6,6 +6,7 @@ import "C" import ( "bytes" "encoding/binary" + "errors" "fmt" "io" "math" @@ -13,6 +14,7 @@ import ( "runtime/cgo" "sync" "sync/atomic" + "time" "unsafe" ) @@ -335,6 +337,7 @@ func readFloat64(reader io.Reader) float64 { func init() { FfiConverterDataHandlerINSTANCE.register() + FfiConverterDataHandlerOldINSTANCE.register() FfiConverterGoSignerINSTANCE.register() uniffiCheckChecksums() } @@ -368,6 +371,15 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_sign: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_func_subscribe_item_debug() + }) + if checksum != 8233 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_func_subscribe_item_debug: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_datahandler_handle_data() @@ -377,6 +389,51 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_datahandler_handle_data: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data() + }) + if checksum != 20343 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_db_iter_with_opts() + }) + if checksum != 3815 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_db_iter_with_opts: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_db_subscribe() + }) + if checksum != 415 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_db_subscribe: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_db_subscribe_with_opts() + }) + if checksum != 17612 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_db_subscribe_with_opts: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_db_write() + }) + if checksum != 5247 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_db_write: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_endpoint_node_addr() @@ -386,6 +443,69 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_endpoint_node_addr: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_global() + }) + if checksum != 47323 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_global: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_scope() + }) + if checksum != 23022 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_scope: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_scopes() + }) + if checksum != 39037 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_scopes: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_stream() + }) + if checksum != 37766 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_stream: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_time_from() + }) + if checksum != 62275 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_time_from: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_time_range() + }) + if checksum != 2045 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_time_range: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_filter_timestamps() + }) + if checksum != 27584 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_filter_timestamps: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_gosigner_sign() @@ -395,6 +515,78 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_gosigner_sign: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_db() + }) + if checksum != 39096 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_db: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_join_peers() + }) + if checksum != 56647 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_join_peers: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_node_id() + }) + if checksum != 55009 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_node_id: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_node_scope() + }) + if checksum != 16912 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_node_scope: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_send_segment() + }) + if checksum != 18989 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_send_segment: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_subscribe() + }) + if checksum != 17951 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_subscribe: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_ticket() + }) + if checksum != 37020 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_ticket: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_unsubscribe() + }) + if checksum != 35396 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_unsubscribe: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_nodeaddr_direct_addresses() @@ -417,7 +609,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_nodeaddr_node_id() }) - if checksum != 35476 { + if checksum != 22953 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_nodeaddr_node_id: UniFFI API checksum mismatch") } @@ -431,6 +623,15 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_nodeaddr_relay_url: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_publickey_as_vec() + }) + if checksum != 32346 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickey_as_vec: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_publickey_equal() @@ -451,11 +652,29 @@ func uniffiCheckChecksums() { } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_publickey_to_bytes() + return C.uniffi_iroh_streamplace_checksum_method_publickeyold_equal() + }) + if checksum != 35079 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_equal: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short() + }) + if checksum != 26426 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes() }) - if checksum != 8223 { + if checksum != 41746 { // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickey_to_bytes: UniFFI API checksum mismatch") + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes: UniFFI API checksum mismatch") } } { @@ -471,7 +690,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_receiver_subscribe() }) - if checksum != 24145 { + if checksum != 46188 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_subscribe: UniFFI API checksum mismatch") } @@ -480,7 +699,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe() }) - if checksum != 21760 { + if checksum != 7879 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe: UniFFI API checksum mismatch") } @@ -503,6 +722,24 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_sender_send: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_subscriberesponse_next_raw() + }) + if checksum != 55650 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_subscriberesponse_next_raw: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_writescope_put() + }) + if checksum != 50543 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_writescope_put: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_endpoint_new() @@ -512,11 +749,47 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_endpoint_new: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_filter_new() + }) + if checksum != 5241 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_filter_new: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_node_forwarder() + }) + if checksum != 7334 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_node_forwarder: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_node_receiver() + }) + if checksum != 33844 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_node_receiver: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_node_sender() + }) + if checksum != 44785 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_node_sender: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_nodeaddr_new() }) - if checksum != 28044 { + if checksum != 56594 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_nodeaddr_new: UniFFI API checksum mismatch") } @@ -525,7 +798,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_publickey_from_bytes() }) - if checksum != 54187 { + if checksum != 57602 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickey_from_bytes: UniFFI API checksum mismatch") } @@ -534,16 +807,34 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_publickey_from_string() }) - if checksum != 62773 { + if checksum != 45922 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickey_from_string: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes() + }) + if checksum != 40751 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string() + }) + if checksum != 54251 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_receiver_new() }) - if checksum != 35153 { + if checksum != 18072 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_receiver_new: UniFFI API checksum mismatch") } @@ -559,6 +850,30 @@ func uniffiCheckChecksums() { } } +type FfiConverterUint64 struct{} + +var FfiConverterUint64INSTANCE = FfiConverterUint64{} + +func (FfiConverterUint64) Lower(value uint64) C.uint64_t { + return C.uint64_t(value) +} + +func (FfiConverterUint64) Write(writer io.Writer, value uint64) { + writeUint64(writer, value) +} + +func (FfiConverterUint64) Lift(value C.uint64_t) uint64 { + return uint64(value) +} + +func (FfiConverterUint64) Read(reader io.Reader) uint64 { + return readUint64(reader) +} + +type FfiDestroyerUint64 struct{} + +func (FfiDestroyerUint64) Destroy(_ uint64) {} + type FfiConverterBool struct{} var FfiConverterBoolINSTANCE = FfiConverterBool{} @@ -608,7 +923,7 @@ func (FfiConverterString) Read(reader io.Reader) string { length := readInt32(reader) buffer := make([]byte, length) read_length, err := reader.Read(buffer) - if err != nil { + if err != nil && err != io.EOF { panic(err) } if read_length != int(length) { @@ -671,7 +986,7 @@ func (c FfiConverterBytes) Read(reader io.Reader) []byte { length := readInt32(reader) buffer := make([]byte, length) read_length, err := reader.Read(buffer) - if err != nil { + if err != nil && err != io.EOF { panic(err) } if read_length != int(length) { @@ -684,6 +999,44 @@ type FfiDestroyerBytes struct{} func (FfiDestroyerBytes) Destroy(_ []byte) {} +// FfiConverterDuration converts between uniffi duration and Go duration. +type FfiConverterDuration struct{} + +var FfiConverterDurationINSTANCE = FfiConverterDuration{} + +func (c FfiConverterDuration) Lift(rb RustBufferI) time.Duration { + return LiftFromRustBuffer[time.Duration](c, rb) +} + +func (c FfiConverterDuration) Read(reader io.Reader) time.Duration { + sec := readUint64(reader) + nsec := readUint32(reader) + return time.Duration(sec*1_000_000_000 + uint64(nsec)) +} + +func (c FfiConverterDuration) Lower(value time.Duration) C.RustBuffer { + return LowerIntoRustBuffer[time.Duration](c, value) +} + +func (c FfiConverterDuration) Write(writer io.Writer, value time.Duration) { + if value.Nanoseconds() < 0 { + // Rust does not support negative durations: + // https://www.reddit.com/r/rust/comments/ljl55u/why_rusts_duration_not_supporting_negative_values/ + // This panic is very bad, because it depends on user input, and in Go user input related + // error are supposed to be returned as errors, and not cause panics. However, with the + // current architecture, its not possible to return an error from here, so panic is used as + // the only other option to signal an error. + panic("negative duration is not allowed") + } + + writeUint64(writer, uint64(value)/1_000_000_000) + writeUint32(writer, uint32(uint64(value)%1_000_000_000)) +} + +type FfiDestroyerDuration struct{} + +func (FfiDestroyerDuration) Destroy(_ time.Duration) {} + // Below is an implementation of synchronization requirements outlined in the link. // https://github.com/mozilla/uniffi-rs/blob/0dc031132d9493ca812c3af6e7dd60ad2ea95bf0/uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt#L31 @@ -747,9 +1100,12 @@ func (ffiObject *FfiObject) freeRustArcPtr() { }) } +// DataHandler trait that is exported to go for receiving data callbacks. type DataHandler interface { HandleData(topic string, data []byte) } + +// DataHandler trait that is exported to go for receiving data callbacks. type DataHandlerImpl struct { ffiObject FfiObject } @@ -757,7 +1113,7 @@ type DataHandlerImpl struct { func (_self *DataHandlerImpl) HandleData(topic string, data []byte) { _pointer := _self.ffiObject.incrementPointer("DataHandler") defer _self.ffiObject.decrementPointer() - uniffiRustCallAsync[struct{}]( + uniffiRustCallAsync[error]( nil, // completeFn func(handle C.uint64_t, status *C.RustCallStatus) struct{} { @@ -940,174 +1296,72 @@ func (c FfiConverterDataHandler) register() { C.uniffi_iroh_streamplace_fn_init_callback_vtable_datahandler(&UniffiVTableCallbackInterfaceDataHandlerINSTANCE) } -type EndpointInterface interface { - NodeAddr() *NodeAddr +type DataHandlerOld interface { + HandleData(topic string, data []byte) } -type Endpoint struct { +type DataHandlerOldImpl struct { ffiObject FfiObject } -// Create a new endpoint. -func NewEndpoint() (*Endpoint, *Error) { - res, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, +func (_self *DataHandlerOldImpl) HandleData(topic string, data []byte) { + _pointer := _self.ffiObject.incrementPointer("DataHandlerOld") + defer _self.ffiObject.decrementPointer() + uniffiRustCallAsync[error]( + nil, // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} }, // liftFn - func(ffi unsafe.Pointer) *Endpoint { - return FfiConverterEndpointINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_constructor_endpoint_new(), + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_datahandlerold_handle_data( + _pointer, FfiConverterStringINSTANCE.Lower(topic), FfiConverterBytesINSTANCE.Lower(data)), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) }, // freeFn func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + C.ffi_iroh_streamplace_rust_future_free_void(handle) }, ) - return res, err +} +func (object *DataHandlerOldImpl) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() } -func (_self *Endpoint) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Endpoint") - defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[struct{}]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_method_endpoint_node_addr( - _pointer), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res -} -func (object *Endpoint) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterEndpoint struct{} - -var FfiConverterEndpointINSTANCE = FfiConverterEndpoint{} - -func (c FfiConverterEndpoint) Lift(pointer unsafe.Pointer) *Endpoint { - result := &Endpoint{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_endpoint(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_endpoint(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*Endpoint).Destroy) - return result -} - -func (c FfiConverterEndpoint) Read(reader io.Reader) *Endpoint { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterEndpoint) Lower(value *Endpoint) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Endpoint") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterEndpoint) Write(writer io.Writer, value *Endpoint) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerEndpoint struct{} - -func (_ FfiDestroyerEndpoint) Destroy(value *Endpoint) { - value.Destroy() -} - -type GoSigner interface { - Sign(data []byte) ([]byte, *SpError) -} -type GoSignerImpl struct { - ffiObject FfiObject -} - -func (_self *GoSignerImpl) Sign(data []byte) ([]byte, *SpError) { - _pointer := _self.ffiObject.incrementPointer("GoSigner") - defer _self.ffiObject.decrementPointer() - _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_gosigner_sign( - _pointer, FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), - } - }) - if _uniffiErr != nil { - var _uniffiDefaultValue []byte - return _uniffiDefaultValue, _uniffiErr - } else { - return FfiConverterBytesINSTANCE.Lift(_uniffiRV), _uniffiErr - } -} -func (object *GoSignerImpl) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterGoSigner struct { - handleMap *concurrentHandleMap[GoSigner] +type FfiConverterDataHandlerOld struct { + handleMap *concurrentHandleMap[DataHandlerOld] } -var FfiConverterGoSignerINSTANCE = FfiConverterGoSigner{ - handleMap: newConcurrentHandleMap[GoSigner](), +var FfiConverterDataHandlerOldINSTANCE = FfiConverterDataHandlerOld{ + handleMap: newConcurrentHandleMap[DataHandlerOld](), } -func (c FfiConverterGoSigner) Lift(pointer unsafe.Pointer) GoSigner { - result := &GoSignerImpl{ +func (c FfiConverterDataHandlerOld) Lift(pointer unsafe.Pointer) DataHandlerOld { + result := &DataHandlerOldImpl{ newFfiObject( pointer, func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_gosigner(pointer, status) + return C.uniffi_iroh_streamplace_fn_clone_datahandlerold(pointer, status) }, func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_gosigner(pointer, status) + C.uniffi_iroh_streamplace_fn_free_datahandlerold(pointer, status) }, ), } - runtime.SetFinalizer(result, (*GoSignerImpl).Destroy) + runtime.SetFinalizer(result, (*DataHandlerOldImpl).Destroy) return result } -func (c FfiConverterGoSigner) Read(reader io.Reader) GoSigner { +func (c FfiConverterDataHandlerOld) Read(reader io.Reader) DataHandlerOld { return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) } -func (c FfiConverterGoSigner) Lower(value GoSigner) unsafe.Pointer { +func (c FfiConverterDataHandlerOld) Lower(value DataHandlerOld) unsafe.Pointer { // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, // because the pointer will be decremented immediately after this function returns, // and someone will be left holding onto a non-locked pointer. @@ -1116,340 +1370,215 @@ func (c FfiConverterGoSigner) Lower(value GoSigner) unsafe.Pointer { } -func (c FfiConverterGoSigner) Write(writer io.Writer, value GoSigner) { +func (c FfiConverterDataHandlerOld) Write(writer io.Writer, value DataHandlerOld) { writeUint64(writer, uint64(uintptr(c.Lower(value)))) } -type FfiDestroyerGoSigner struct{} +type FfiDestroyerDataHandlerOld struct{} -func (_ FfiDestroyerGoSigner) Destroy(value GoSigner) { - if val, ok := value.(*GoSignerImpl); ok { +func (_ FfiDestroyerDataHandlerOld) Destroy(value DataHandlerOld) { + if val, ok := value.(*DataHandlerOldImpl); ok { val.Destroy() } else { - panic("Expected *GoSignerImpl") + panic("Expected *DataHandlerOldImpl") } } -//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0 -func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uniffiHandle C.uint64_t, data C.RustBuffer, uniffiOutReturn *C.RustBuffer, callStatus *C.RustCallStatus) { +//export iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0 +func iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0(uniffiHandle C.uint64_t, topic C.RustBuffer, data C.RustBuffer, uniffiFutureCallback C.UniffiForeignFutureCompleteVoid, uniffiCallbackData C.uint64_t, uniffiOutReturn *C.UniffiForeignFuture) { handle := uint64(uniffiHandle) - uniffiObj, ok := FfiConverterGoSignerINSTANCE.handleMap.tryGet(handle) + uniffiObj, ok := FfiConverterDataHandlerOldINSTANCE.handleMap.tryGet(handle) if !ok { panic(fmt.Errorf("no callback in handle map: %d", handle)) } - res, err := - uniffiObj.Sign( + result := make(chan C.UniffiForeignFutureStructVoid, 1) + cancel := make(chan struct{}, 1) + guardHandle := cgo.NewHandle(cancel) + *uniffiOutReturn = C.UniffiForeignFuture{ + handle: C.uint64_t(guardHandle), + free: C.UniffiForeignFutureFree(C.iroh_streamplace_uniffiFreeGorutine), + } + + // Wait for compleation or cancel + go func() { + select { + case <-cancel: + case res := <-result: + C.call_UniffiForeignFutureCompleteVoid(uniffiFutureCallback, uniffiCallbackData, res) + } + }() + + // Eval callback asynchroniously + go func() { + asyncResult := &C.UniffiForeignFutureStructVoid{} + defer func() { + result <- *asyncResult + }() + + uniffiObj.HandleData( + FfiConverterStringINSTANCE.Lift(GoRustBuffer{ + inner: topic, + }), FfiConverterBytesINSTANCE.Lift(GoRustBuffer{ inner: data, }), ) - if err != nil { - // The only way to bypass an unexpected error is to bypass pointer to an empty - // instance of the error - if err.err == nil { - *callStatus = C.RustCallStatus{ - code: C.int8_t(uniffiCallbackUnexpectedResultError), - } - return - } - - *callStatus = C.RustCallStatus{ - code: C.int8_t(uniffiCallbackResultError), - errorBuf: FfiConverterSpErrorINSTANCE.Lower(err), - } - return - } - - *uniffiOutReturn = FfiConverterBytesINSTANCE.Lower(res) + }() } -var UniffiVTableCallbackInterfaceGoSignerINSTANCE = C.UniffiVTableCallbackInterfaceGoSigner{ - sign: (C.UniffiCallbackInterfaceGoSignerMethod0)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0), +var UniffiVTableCallbackInterfaceDataHandlerOldINSTANCE = C.UniffiVTableCallbackInterfaceDataHandlerOld{ + handleData: (C.UniffiCallbackInterfaceDataHandlerOldMethod0)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0), - uniffiFree: (C.UniffiCallbackInterfaceFree)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree), + uniffiFree: (C.UniffiCallbackInterfaceFree)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree), } -//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree -func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(handle C.uint64_t) { - FfiConverterGoSignerINSTANCE.handleMap.remove(uint64(handle)) +//export iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree +func iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree(handle C.uint64_t) { + FfiConverterDataHandlerOldINSTANCE.handleMap.remove(uint64(handle)) } -func (c FfiConverterGoSigner) register() { - C.uniffi_iroh_streamplace_fn_init_callback_vtable_gosigner(&UniffiVTableCallbackInterfaceGoSignerINSTANCE) +func (c FfiConverterDataHandlerOld) register() { + C.uniffi_iroh_streamplace_fn_init_callback_vtable_datahandlerold(&UniffiVTableCallbackInterfaceDataHandlerOldINSTANCE) } -// A peer and it's addressing information. -type NodeAddrInterface interface { - // Get the direct addresses of this peer. - DirectAddresses() []string - // Returns true if both NodeAddr's have the same values - Equal(other *NodeAddr) bool - NodeId() *PublicKey - // Get the home relay URL for this peer - RelayUrl() *string +// Iroh-streamplace specific metadata database. +type DbInterface interface { + IterWithOpts(filter *Filter) ([]Entry, error) + Subscribe(filter *Filter) *SubscribeResponse + // Subscribe with options. + SubscribeWithOpts(opts SubscribeOpts) *SubscribeResponse + Write(secret []byte) (*WriteScope, error) } -// A peer and it's addressing information. -type NodeAddr struct { +// Iroh-streamplace specific metadata database. +type Db struct { ffiObject FfiObject } -// Create a new [`NodeAddr`] with empty [`AddrInfo`]. -func NewNodeAddr(nodeId *PublicKey, derpUrl *string, addresses []string) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_nodeaddr_new(FfiConverterPublicKeyINSTANCE.Lower(nodeId), FfiConverterOptionalStringINSTANCE.Lower(derpUrl), FfiConverterSequenceStringINSTANCE.Lower(addresses), _uniffiStatus) - })) -} - -// Get the direct addresses of this peer. -func (_self *NodeAddr) DirectAddresses() []string { - _pointer := _self.ffiObject.incrementPointer("*NodeAddr") +func (_self *Db) IterWithOpts(filter *Filter) ([]Entry, error) { + _pointer := _self.ffiObject.incrementPointer("*Db") defer _self.ffiObject.decrementPointer() - return FfiConverterSequenceStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_nodeaddr_direct_addresses( - _pointer, _uniffiStatus), - } - })) + res, err := uniffiRustCallAsync[SubscribeNextError]( + FfiConverterSubscribeNextErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) RustBufferI { + res := C.ffi_iroh_streamplace_rust_future_complete_rust_buffer(handle, status) + return GoRustBuffer{ + inner: res, + } + }, + // liftFn + func(ffi RustBufferI) []Entry { + return FfiConverterSequenceEntryINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_db_iter_with_opts( + _pointer, FfiConverterFilterINSTANCE.Lower(filter)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_rust_buffer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_rust_buffer(handle) + }, + ) + + return res, err } -// Returns true if both NodeAddr's have the same values -func (_self *NodeAddr) Equal(other *NodeAddr) bool { - _pointer := _self.ffiObject.incrementPointer("*NodeAddr") +func (_self *Db) Subscribe(filter *Filter) *SubscribeResponse { + _pointer := _self.ffiObject.incrementPointer("*Db") defer _self.ffiObject.decrementPointer() - return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { - return C.uniffi_iroh_streamplace_fn_method_nodeaddr_equal( - _pointer, FfiConverterNodeAddrINSTANCE.Lower(other), _uniffiStatus) + return FfiConverterSubscribeResponseINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_db_subscribe( + _pointer, FfiConverterFilterINSTANCE.Lower(filter), _uniffiStatus) })) } -func (_self *NodeAddr) NodeId() *PublicKey { - _pointer := _self.ffiObject.incrementPointer("*NodeAddr") +// Subscribe with options. +func (_self *Db) SubscribeWithOpts(opts SubscribeOpts) *SubscribeResponse { + _pointer := _self.ffiObject.incrementPointer("*Db") defer _self.ffiObject.decrementPointer() - return FfiConverterPublicKeyINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_method_nodeaddr_node_id( - _pointer, _uniffiStatus) + return FfiConverterSubscribeResponseINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_db_subscribe_with_opts( + _pointer, FfiConverterSubscribeOptsINSTANCE.Lower(opts), _uniffiStatus) })) } -// Get the home relay URL for this peer -func (_self *NodeAddr) RelayUrl() *string { - _pointer := _self.ffiObject.incrementPointer("*NodeAddr") +func (_self *Db) Write(secret []byte) (*WriteScope, error) { + _pointer := _self.ffiObject.incrementPointer("*Db") defer _self.ffiObject.decrementPointer() - return FfiConverterOptionalStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_nodeaddr_relay_url( - _pointer, _uniffiStatus), - } - })) + _uniffiRV, _uniffiErr := rustCallWithError[WriteError](FfiConverterWriteError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_db_write( + _pointer, FfiConverterBytesINSTANCE.Lower(secret), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *WriteScope + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterWriteScopeINSTANCE.Lift(_uniffiRV), nil + } } -func (object *NodeAddr) Destroy() { +func (object *Db) Destroy() { runtime.SetFinalizer(object, nil) object.ffiObject.destroy() } -type FfiConverterNodeAddr struct{} +type FfiConverterDb struct{} -var FfiConverterNodeAddrINSTANCE = FfiConverterNodeAddr{} +var FfiConverterDbINSTANCE = FfiConverterDb{} -func (c FfiConverterNodeAddr) Lift(pointer unsafe.Pointer) *NodeAddr { - result := &NodeAddr{ +func (c FfiConverterDb) Lift(pointer unsafe.Pointer) *Db { + result := &Db{ newFfiObject( pointer, func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_nodeaddr(pointer, status) + return C.uniffi_iroh_streamplace_fn_clone_db(pointer, status) }, func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_nodeaddr(pointer, status) + C.uniffi_iroh_streamplace_fn_free_db(pointer, status) }, ), } - runtime.SetFinalizer(result, (*NodeAddr).Destroy) + runtime.SetFinalizer(result, (*Db).Destroy) return result } -func (c FfiConverterNodeAddr) Read(reader io.Reader) *NodeAddr { +func (c FfiConverterDb) Read(reader io.Reader) *Db { return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) } -func (c FfiConverterNodeAddr) Lower(value *NodeAddr) unsafe.Pointer { +func (c FfiConverterDb) Lower(value *Db) unsafe.Pointer { // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, // because the pointer will be decremented immediately after this function returns, // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*NodeAddr") + pointer := value.ffiObject.incrementPointer("*Db") defer value.ffiObject.decrementPointer() return pointer } -func (c FfiConverterNodeAddr) Write(writer io.Writer, value *NodeAddr) { +func (c FfiConverterDb) Write(writer io.Writer, value *Db) { writeUint64(writer, uint64(uintptr(c.Lower(value)))) } -type FfiDestroyerNodeAddr struct{} +type FfiDestroyerDb struct{} -func (_ FfiDestroyerNodeAddr) Destroy(value *NodeAddr) { +func (_ FfiDestroyerDb) Destroy(value *Db) { value.Destroy() } -// A public key. -// -// The key itself is just a 32 byte array, but a key has associated crypto -// information that is cached for performance reasons. -type PublicKeyInterface interface { - // Returns true if the PublicKeys are equal - Equal(other *PublicKey) bool - // Convert to a base32 string limited to the first 10 bytes for a friendly string - // representation of the key. - FmtShort() string - // Express the PublicKey as a byte array - ToBytes() []byte +type EndpointInterface interface { + NodeAddr() *NodeAddr +} +type Endpoint struct { + ffiObject FfiObject } -// A public key. -// -// The key itself is just a 32 byte array, but a key has associated crypto -// information that is cached for performance reasons. -type PublicKey struct { - ffiObject FfiObject -} - -// Make a PublicKey from byte array -func PublicKeyFromBytes(bytes []byte) (*PublicKey, *Error) { - _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_publickey_from_bytes(FfiConverterBytesINSTANCE.Lower(bytes), _uniffiStatus) - }) - if _uniffiErr != nil { - var _uniffiDefaultValue *PublicKey - return _uniffiDefaultValue, _uniffiErr - } else { - return FfiConverterPublicKeyINSTANCE.Lift(_uniffiRV), _uniffiErr - } -} - -// Make a PublicKey from base32 string -func PublicKeyFromString(s string) (*PublicKey, *Error) { - _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_publickey_from_string(FfiConverterStringINSTANCE.Lower(s), _uniffiStatus) - }) - if _uniffiErr != nil { - var _uniffiDefaultValue *PublicKey - return _uniffiDefaultValue, _uniffiErr - } else { - return FfiConverterPublicKeyINSTANCE.Lift(_uniffiRV), _uniffiErr - } -} - -// Returns true if the PublicKeys are equal -func (_self *PublicKey) Equal(other *PublicKey) bool { - _pointer := _self.ffiObject.incrementPointer("*PublicKey") - defer _self.ffiObject.decrementPointer() - return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { - return C.uniffi_iroh_streamplace_fn_method_publickey_equal( - _pointer, FfiConverterPublicKeyINSTANCE.Lower(other), _uniffiStatus) - })) -} - -// Convert to a base32 string limited to the first 10 bytes for a friendly string -// representation of the key. -func (_self *PublicKey) FmtShort() string { - _pointer := _self.ffiObject.incrementPointer("*PublicKey") - defer _self.ffiObject.decrementPointer() - return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickey_fmt_short( - _pointer, _uniffiStatus), - } - })) -} - -// Express the PublicKey as a byte array -func (_self *PublicKey) ToBytes() []byte { - _pointer := _self.ffiObject.incrementPointer("*PublicKey") - defer _self.ffiObject.decrementPointer() - return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickey_to_bytes( - _pointer, _uniffiStatus), - } - })) -} - -func (_self *PublicKey) String() string { - _pointer := _self.ffiObject.incrementPointer("*PublicKey") - defer _self.ffiObject.decrementPointer() - return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickey_uniffi_trait_display( - _pointer, _uniffiStatus), - } - })) -} - -func (object *PublicKey) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterPublicKey struct{} - -var FfiConverterPublicKeyINSTANCE = FfiConverterPublicKey{} - -func (c FfiConverterPublicKey) Lift(pointer unsafe.Pointer) *PublicKey { - result := &PublicKey{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_publickey(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_publickey(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*PublicKey).Destroy) - return result -} - -func (c FfiConverterPublicKey) Read(reader io.Reader) *PublicKey { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterPublicKey) Lower(value *PublicKey) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*PublicKey") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterPublicKey) Write(writer io.Writer, value *PublicKey) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerPublicKey struct{} - -func (_ FfiDestroyerPublicKey) Destroy(value *PublicKey) { - value.Destroy() -} - -type ReceiverInterface interface { - NodeAddr() *NodeAddr - // Subscribe to the given topic on the remote. - Subscribe(remoteId *PublicKey, topic string) *Error - // Unsubscribe from this topic on the remote. - Unsubscribe(remoteId *PublicKey, topic string) *Error -} -type Receiver struct { - ffiObject FfiObject -} - -// Create a new receiver. -func NewReceiver(endpoint *Endpoint, handler DataHandler) (*Receiver, *Error) { +// Create a new endpoint. +func NewEndpoint() (*Endpoint, error) { res, err := uniffiRustCallAsync[Error]( FfiConverterErrorINSTANCE, // completeFn @@ -1458,10 +1587,10 @@ func NewReceiver(endpoint *Endpoint, handler DataHandler) (*Receiver, *Error) { return res }, // liftFn - func(ffi unsafe.Pointer) *Receiver { - return FfiConverterReceiverINSTANCE.Lift(ffi) + func(ffi unsafe.Pointer) *Endpoint { + return FfiConverterEndpointINSTANCE.Lift(ffi) }, - C.uniffi_iroh_streamplace_fn_constructor_receiver_new(FfiConverterEndpointINSTANCE.Lower(endpoint), FfiConverterDataHandlerINSTANCE.Lower(handler)), + C.uniffi_iroh_streamplace_fn_constructor_endpoint_new(), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) @@ -1475,10 +1604,10 @@ func NewReceiver(endpoint *Endpoint, handler DataHandler) (*Receiver, *Error) { return res, err } -func (_self *Receiver) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Receiver") +func (_self *Endpoint) NodeAddr() *NodeAddr { + _pointer := _self.ffiObject.incrementPointer("*Endpoint") defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[struct{}]( + res, _ := uniffiRustCallAsync[error]( nil, // completeFn func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { @@ -1489,7 +1618,7 @@ func (_self *Receiver) NodeAddr() *NodeAddr { func(ffi unsafe.Pointer) *NodeAddr { return FfiConverterNodeAddrINSTANCE.Lift(ffi) }, - C.uniffi_iroh_streamplace_fn_method_receiver_node_addr( + C.uniffi_iroh_streamplace_fn_method_endpoint_node_addr( _pointer), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { @@ -1503,163 +1632,423 @@ func (_self *Receiver) NodeAddr() *NodeAddr { return res } - -// Subscribe to the given topic on the remote. -func (_self *Receiver) Subscribe(remoteId *PublicKey, topic string) *Error { - _pointer := _self.ffiObject.incrementPointer("*Receiver") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_receiver_subscribe( - _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - - return err -} - -// Unsubscribe from this topic on the remote. -func (_self *Receiver) Unsubscribe(remoteId *PublicKey, topic string) *Error { - _pointer := _self.ffiObject.incrementPointer("*Receiver") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_receiver_unsubscribe( - _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - - return err -} -func (object *Receiver) Destroy() { +func (object *Endpoint) Destroy() { runtime.SetFinalizer(object, nil) object.ffiObject.destroy() } -type FfiConverterReceiver struct{} +type FfiConverterEndpoint struct{} -var FfiConverterReceiverINSTANCE = FfiConverterReceiver{} +var FfiConverterEndpointINSTANCE = FfiConverterEndpoint{} -func (c FfiConverterReceiver) Lift(pointer unsafe.Pointer) *Receiver { - result := &Receiver{ +func (c FfiConverterEndpoint) Lift(pointer unsafe.Pointer) *Endpoint { + result := &Endpoint{ newFfiObject( pointer, func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_receiver(pointer, status) + return C.uniffi_iroh_streamplace_fn_clone_endpoint(pointer, status) }, func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_receiver(pointer, status) + C.uniffi_iroh_streamplace_fn_free_endpoint(pointer, status) }, ), } - runtime.SetFinalizer(result, (*Receiver).Destroy) + runtime.SetFinalizer(result, (*Endpoint).Destroy) return result } -func (c FfiConverterReceiver) Read(reader io.Reader) *Receiver { +func (c FfiConverterEndpoint) Read(reader io.Reader) *Endpoint { return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) } -func (c FfiConverterReceiver) Lower(value *Receiver) unsafe.Pointer { +func (c FfiConverterEndpoint) Lower(value *Endpoint) unsafe.Pointer { // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, // because the pointer will be decremented immediately after this function returns, // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Receiver") + pointer := value.ffiObject.incrementPointer("*Endpoint") defer value.ffiObject.decrementPointer() return pointer } -func (c FfiConverterReceiver) Write(writer io.Writer, value *Receiver) { +func (c FfiConverterEndpoint) Write(writer io.Writer, value *Endpoint) { writeUint64(writer, uint64(uintptr(c.Lower(value)))) } -type FfiDestroyerReceiver struct{} +type FfiDestroyerEndpoint struct{} -func (_ FfiDestroyerReceiver) Destroy(value *Receiver) { +func (_ FfiDestroyerEndpoint) Destroy(value *Endpoint) { value.Destroy() } -type SenderInterface interface { - NodeAddr() *NodeAddr - // Sends the given data to all subscribers that have subscribed to this `key`. - Send(key string, data []byte) *Error -} -type Sender struct { +// A filter for subscriptions and iteration. +type FilterInterface interface { + // Restrict to the global namespace, no per stream data. + Global() *Filter + // Restrict to a single scope. + Scope(scope *PublicKey) *Filter + // Restrict to a set of scopes. + Scopes(scopes []*PublicKey) *Filter + // Restrict to one specific stream, no global data. + Stream(stream []byte) *Filter + // Restrict to a time range starting at min, unbounded at the top. + TimeFrom(min uint64) *Filter + // Restrict to a time range given in nanoseconds since unix epoch. + TimeRange(min uint64, max uint64) *Filter + // Restrict to a time range. + Timestamps(min TimeBound, max TimeBound) *Filter +} + +// A filter for subscriptions and iteration. +type Filter struct { ffiObject FfiObject } -// Create a new sender. -func NewSender(endpoint *Endpoint) (*Sender, *Error) { - res, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *Sender { - return FfiConverterSenderINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_constructor_sender_new(FfiConverterEndpointINSTANCE.Lower(endpoint)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res, err +// Creates a new filter that matches everything. +func NewFilter() *Filter { + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_filter_new(_uniffiStatus) + })) } -func (_self *Sender) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Sender") +// Restrict to the global namespace, no per stream data. +func (_self *Filter) Global() *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[struct{}]( - nil, + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_global( + _pointer, _uniffiStatus) + })) +} + +// Restrict to a single scope. +func (_self *Filter) Scope(scope *PublicKey) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_scope( + _pointer, FfiConverterPublicKeyINSTANCE.Lower(scope), _uniffiStatus) + })) +} + +// Restrict to a set of scopes. +func (_self *Filter) Scopes(scopes []*PublicKey) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_scopes( + _pointer, FfiConverterSequencePublicKeyINSTANCE.Lower(scopes), _uniffiStatus) + })) +} + +// Restrict to one specific stream, no global data. +func (_self *Filter) Stream(stream []byte) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_stream( + _pointer, FfiConverterBytesINSTANCE.Lower(stream), _uniffiStatus) + })) +} + +// Restrict to a time range starting at min, unbounded at the top. +func (_self *Filter) TimeFrom(min uint64) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_time_from( + _pointer, FfiConverterUint64INSTANCE.Lower(min), _uniffiStatus) + })) +} + +// Restrict to a time range given in nanoseconds since unix epoch. +func (_self *Filter) TimeRange(min uint64, max uint64) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_time_range( + _pointer, FfiConverterUint64INSTANCE.Lower(min), FfiConverterUint64INSTANCE.Lower(max), _uniffiStatus) + })) +} + +// Restrict to a time range. +func (_self *Filter) Timestamps(min TimeBound, max TimeBound) *Filter { + _pointer := _self.ffiObject.incrementPointer("*Filter") + defer _self.ffiObject.decrementPointer() + return FfiConverterFilterINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_filter_timestamps( + _pointer, FfiConverterTimeBoundINSTANCE.Lower(min), FfiConverterTimeBoundINSTANCE.Lower(max), _uniffiStatus) + })) +} +func (object *Filter) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterFilter struct{} + +var FfiConverterFilterINSTANCE = FfiConverterFilter{} + +func (c FfiConverterFilter) Lift(pointer unsafe.Pointer) *Filter { + result := &Filter{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_filter(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_filter(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*Filter).Destroy) + return result +} + +func (c FfiConverterFilter) Read(reader io.Reader) *Filter { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterFilter) Lower(value *Filter) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Filter") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterFilter) Write(writer io.Writer, value *Filter) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerFilter struct{} + +func (_ FfiDestroyerFilter) Destroy(value *Filter) { + value.Destroy() +} + +type GoSigner interface { + Sign(data []byte) ([]byte, error) +} +type GoSignerImpl struct { + ffiObject FfiObject +} + +func (_self *GoSignerImpl) Sign(data []byte) ([]byte, error) { + _pointer := _self.ffiObject.incrementPointer("GoSigner") + defer _self.ffiObject.decrementPointer() + _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_gosigner_sign( + _pointer, FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), + } + }) + if _uniffiErr != nil { + var _uniffiDefaultValue []byte + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterBytesINSTANCE.Lift(_uniffiRV), nil + } +} +func (object *GoSignerImpl) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterGoSigner struct { + handleMap *concurrentHandleMap[GoSigner] +} + +var FfiConverterGoSignerINSTANCE = FfiConverterGoSigner{ + handleMap: newConcurrentHandleMap[GoSigner](), +} + +func (c FfiConverterGoSigner) Lift(pointer unsafe.Pointer) GoSigner { + result := &GoSignerImpl{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_gosigner(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_gosigner(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*GoSignerImpl).Destroy) + return result +} + +func (c FfiConverterGoSigner) Read(reader io.Reader) GoSigner { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterGoSigner) Lower(value GoSigner) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := unsafe.Pointer(uintptr(c.handleMap.insert(value))) + return pointer + +} + +func (c FfiConverterGoSigner) Write(writer io.Writer, value GoSigner) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerGoSigner struct{} + +func (_ FfiDestroyerGoSigner) Destroy(value GoSigner) { + if val, ok := value.(*GoSignerImpl); ok { + val.Destroy() + } else { + panic("Expected *GoSignerImpl") + } +} + +//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0 +func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uniffiHandle C.uint64_t, data C.RustBuffer, uniffiOutReturn *C.RustBuffer, callStatus *C.RustCallStatus) { + handle := uint64(uniffiHandle) + uniffiObj, ok := FfiConverterGoSignerINSTANCE.handleMap.tryGet(handle) + if !ok { + panic(fmt.Errorf("no callback in handle map: %d", handle)) + } + + res, err := + uniffiObj.Sign( + FfiConverterBytesINSTANCE.Lift(GoRustBuffer{ + inner: data, + }), + ) + + if err != nil { + var actualError *SpError + if errors.As(err, &actualError) { + *callStatus = C.RustCallStatus{ + code: C.int8_t(uniffiCallbackResultError), + errorBuf: FfiConverterSpErrorINSTANCE.Lower(actualError), + } + } else { + *callStatus = C.RustCallStatus{ + code: C.int8_t(uniffiCallbackUnexpectedResultError), + } + } + return + } + + *uniffiOutReturn = FfiConverterBytesINSTANCE.Lower(res) +} + +var UniffiVTableCallbackInterfaceGoSignerINSTANCE = C.UniffiVTableCallbackInterfaceGoSigner{ + sign: (C.UniffiCallbackInterfaceGoSignerMethod0)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0), + + uniffiFree: (C.UniffiCallbackInterfaceFree)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree), +} + +//export iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree +func iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(handle C.uint64_t) { + FfiConverterGoSignerINSTANCE.handleMap.remove(uint64(handle)) +} + +func (c FfiConverterGoSigner) register() { + C.uniffi_iroh_streamplace_fn_init_callback_vtable_gosigner(&UniffiVTableCallbackInterfaceGoSignerINSTANCE) +} + +// Iroh-streamplace node that can send, forward or receive stream segments. +type NodeInterface interface { + // Get a handle to the db to watch for changes locally or globally. + Db() *Db + // Join peers by their node tickets. + JoinPeers(peers []string) error + // Get this node's node ID. + NodeId() (*PublicKey, error) + // Get a handle to the write scope for this node. + // + // This is equivalent to calling `db.write(...)` with the secret key used to create the node. + NodeScope() *WriteScope + // Send a segment to all subscribers of the given stream. + SendSegment(key string, data []byte) error + // Subscribe to updates for a given stream from a remote node. + Subscribe(key string, remoteId *PublicKey) error + // Get this node's ticket. + Ticket() (string, error) + // Unsubscribe from updates for a given stream from a remote node. + Unsubscribe(key string, remoteId *PublicKey) error +} + +// Iroh-streamplace node that can send, forward or receive stream segments. +type Node struct { + ffiObject FfiObject +} + +func NodeForwarder(config Config) (*Node, error) { + res, err := uniffiRustCallAsync[CreateError]( + FfiConverterCreateErrorINSTANCE, // completeFn func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) return res }, // liftFn - func(ffi unsafe.Pointer) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(ffi) + func(ffi unsafe.Pointer) *Node { + return FfiConverterNodeINSTANCE.Lift(ffi) }, - C.uniffi_iroh_streamplace_fn_method_sender_node_addr( - _pointer), + C.uniffi_iroh_streamplace_fn_constructor_node_forwarder(FfiConverterConfigINSTANCE.Lower(config)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res, err +} + +func NodeReceiver(config Config, handler DataHandler) (*Node, error) { + res, err := uniffiRustCallAsync[CreateError]( + FfiConverterCreateErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *Node { + return FfiConverterNodeINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_constructor_node_receiver(FfiConverterConfigINSTANCE.Lower(config), FfiConverterDataHandlerINSTANCE.Lower(handler)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res, err +} + +// Create a new streamplace client node. +func NodeSender(config Config) (*Node, error) { + res, err := uniffiRustCallAsync[CreateError]( + FfiConverterCreateErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *Node { + return FfiConverterNodeINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_constructor_node_sender(FfiConverterConfigINSTANCE.Lower(config)), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) @@ -1670,341 +2059,2705 @@ func (_self *Sender) NodeAddr() *NodeAddr { }, ) - return res + return res, err +} + +// Get a handle to the db to watch for changes locally or globally. +func (_self *Node) Db() *Db { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + return FfiConverterDbINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_node_db( + _pointer, _uniffiStatus) + })) +} + +// Join peers by their node tickets. +func (_self *Node) JoinPeers(peers []string) error { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[JoinPeersError]( + FfiConverterJoinPeersErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_node_join_peers( + _pointer, FfiConverterSequenceStringINSTANCE.Lower(peers)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} + +// Get this node's node ID. +func (_self *Node) NodeId() (*PublicKey, error) { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + res, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *PublicKey { + return FfiConverterPublicKeyINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_node_node_id( + _pointer), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res, err +} + +// Get a handle to the write scope for this node. +// +// This is equivalent to calling `db.write(...)` with the secret key used to create the node. +func (_self *Node) NodeScope() *WriteScope { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + return FfiConverterWriteScopeINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_node_node_scope( + _pointer, _uniffiStatus) + })) +} + +// Send a segment to all subscribers of the given stream. +func (_self *Node) SendSegment(key string, data []byte) error { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_node_send_segment( + _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterBytesINSTANCE.Lower(data)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} + +// Subscribe to updates for a given stream from a remote node. +func (_self *Node) Subscribe(key string, remoteId *PublicKey) error { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_node_subscribe( + _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterPublicKeyINSTANCE.Lower(remoteId)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} + +// Get this node's ticket. +func (_self *Node) Ticket() (string, error) { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + res, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) RustBufferI { + res := C.ffi_iroh_streamplace_rust_future_complete_rust_buffer(handle, status) + return GoRustBuffer{ + inner: res, + } + }, + // liftFn + func(ffi RustBufferI) string { + return FfiConverterStringINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_node_ticket( + _pointer), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_rust_buffer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_rust_buffer(handle) + }, + ) + + return res, err +} + +// Unsubscribe from updates for a given stream from a remote node. +func (_self *Node) Unsubscribe(key string, remoteId *PublicKey) error { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_node_unsubscribe( + _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterPublicKeyINSTANCE.Lower(remoteId)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} +func (object *Node) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterNode struct{} + +var FfiConverterNodeINSTANCE = FfiConverterNode{} + +func (c FfiConverterNode) Lift(pointer unsafe.Pointer) *Node { + result := &Node{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_node(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_node(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*Node).Destroy) + return result +} + +func (c FfiConverterNode) Read(reader io.Reader) *Node { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterNode) Lower(value *Node) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Node") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterNode) Write(writer io.Writer, value *Node) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerNode struct{} + +func (_ FfiDestroyerNode) Destroy(value *Node) { + value.Destroy() +} + +// A peer and it's addressing information. +type NodeAddrInterface interface { + // Get the direct addresses of this peer. + DirectAddresses() []string + // Returns true if both NodeAddr's have the same values + Equal(other *NodeAddr) bool + NodeId() *PublicKeyOld + // Get the home relay URL for this peer + RelayUrl() *string +} + +// A peer and it's addressing information. +type NodeAddr struct { + ffiObject FfiObject +} + +// Create a new [`NodeAddr`] with empty [`AddrInfo`]. +func NewNodeAddr(nodeId *PublicKeyOld, derpUrl *string, addresses []string) *NodeAddr { + return FfiConverterNodeAddrINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_nodeaddr_new(FfiConverterPublicKeyOldINSTANCE.Lower(nodeId), FfiConverterOptionalStringINSTANCE.Lower(derpUrl), FfiConverterSequenceStringINSTANCE.Lower(addresses), _uniffiStatus) + })) +} + +// Get the direct addresses of this peer. +func (_self *NodeAddr) DirectAddresses() []string { + _pointer := _self.ffiObject.incrementPointer("*NodeAddr") + defer _self.ffiObject.decrementPointer() + return FfiConverterSequenceStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_nodeaddr_direct_addresses( + _pointer, _uniffiStatus), + } + })) +} + +// Returns true if both NodeAddr's have the same values +func (_self *NodeAddr) Equal(other *NodeAddr) bool { + _pointer := _self.ffiObject.incrementPointer("*NodeAddr") + defer _self.ffiObject.decrementPointer() + return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { + return C.uniffi_iroh_streamplace_fn_method_nodeaddr_equal( + _pointer, FfiConverterNodeAddrINSTANCE.Lower(other), _uniffiStatus) + })) +} + +func (_self *NodeAddr) NodeId() *PublicKeyOld { + _pointer := _self.ffiObject.incrementPointer("*NodeAddr") + defer _self.ffiObject.decrementPointer() + return FfiConverterPublicKeyOldINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_method_nodeaddr_node_id( + _pointer, _uniffiStatus) + })) +} + +// Get the home relay URL for this peer +func (_self *NodeAddr) RelayUrl() *string { + _pointer := _self.ffiObject.incrementPointer("*NodeAddr") + defer _self.ffiObject.decrementPointer() + return FfiConverterOptionalStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_nodeaddr_relay_url( + _pointer, _uniffiStatus), + } + })) +} +func (object *NodeAddr) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterNodeAddr struct{} + +var FfiConverterNodeAddrINSTANCE = FfiConverterNodeAddr{} + +func (c FfiConverterNodeAddr) Lift(pointer unsafe.Pointer) *NodeAddr { + result := &NodeAddr{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_nodeaddr(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_nodeaddr(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*NodeAddr).Destroy) + return result +} + +func (c FfiConverterNodeAddr) Read(reader io.Reader) *NodeAddr { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterNodeAddr) Lower(value *NodeAddr) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*NodeAddr") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterNodeAddr) Write(writer io.Writer, value *NodeAddr) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerNodeAddr struct{} + +func (_ FfiDestroyerNodeAddr) Destroy(value *NodeAddr) { + value.Destroy() +} + +// A public key. +// +// The key itself is just a 32 byte array, but a key has associated crypto +// information that is cached for performance reasons. +type PublicKeyInterface interface { + // Express the PublicKey as a byte array + AsVec() []byte + // Returns true if the PublicKeys are equal + Equal(other *PublicKey) bool + // Convert to a base32 string limited to the first 10 bytes for a friendly string + // representation of the key. + FmtShort() string +} + +// A public key. +// +// The key itself is just a 32 byte array, but a key has associated crypto +// information that is cached for performance reasons. +type PublicKey struct { + ffiObject FfiObject +} + +// Make a PublicKey from byte array +func PublicKeyFromBytes(bytes []byte) (*PublicKey, error) { + _uniffiRV, _uniffiErr := rustCallWithError[PublicKeyError](FfiConverterPublicKeyError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_publickey_from_bytes(FfiConverterBytesINSTANCE.Lower(bytes), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *PublicKey + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterPublicKeyINSTANCE.Lift(_uniffiRV), nil + } +} + +// Make a PublicKey from base32 string +func PublicKeyFromString(s string) (*PublicKey, error) { + _uniffiRV, _uniffiErr := rustCallWithError[PublicKeyError](FfiConverterPublicKeyError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_publickey_from_string(FfiConverterStringINSTANCE.Lower(s), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *PublicKey + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterPublicKeyINSTANCE.Lift(_uniffiRV), nil + } +} + +// Express the PublicKey as a byte array +func (_self *PublicKey) AsVec() []byte { + _pointer := _self.ffiObject.incrementPointer("*PublicKey") + defer _self.ffiObject.decrementPointer() + return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickey_as_vec( + _pointer, _uniffiStatus), + } + })) +} + +// Returns true if the PublicKeys are equal +func (_self *PublicKey) Equal(other *PublicKey) bool { + _pointer := _self.ffiObject.incrementPointer("*PublicKey") + defer _self.ffiObject.decrementPointer() + return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { + return C.uniffi_iroh_streamplace_fn_method_publickey_equal( + _pointer, FfiConverterPublicKeyINSTANCE.Lower(other), _uniffiStatus) + })) +} + +// Convert to a base32 string limited to the first 10 bytes for a friendly string +// representation of the key. +func (_self *PublicKey) FmtShort() string { + _pointer := _self.ffiObject.incrementPointer("*PublicKey") + defer _self.ffiObject.decrementPointer() + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickey_fmt_short( + _pointer, _uniffiStatus), + } + })) +} + +func (_self *PublicKey) String() string { + _pointer := _self.ffiObject.incrementPointer("*PublicKey") + defer _self.ffiObject.decrementPointer() + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickey_uniffi_trait_display( + _pointer, _uniffiStatus), + } + })) +} + +func (object *PublicKey) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterPublicKey struct{} + +var FfiConverterPublicKeyINSTANCE = FfiConverterPublicKey{} + +func (c FfiConverterPublicKey) Lift(pointer unsafe.Pointer) *PublicKey { + result := &PublicKey{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_publickey(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_publickey(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*PublicKey).Destroy) + return result +} + +func (c FfiConverterPublicKey) Read(reader io.Reader) *PublicKey { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterPublicKey) Lower(value *PublicKey) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*PublicKey") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterPublicKey) Write(writer io.Writer, value *PublicKey) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerPublicKey struct{} + +func (_ FfiDestroyerPublicKey) Destroy(value *PublicKey) { + value.Destroy() +} + +// A public key. +// +// The key itself is just a 32 byte array, but a key has associated crypto +// information that is cached for performance reasons. +type PublicKeyOldInterface interface { + // Returns true if the PublicKeys are equal + Equal(other *PublicKeyOld) bool + // Convert to a base32 string limited to the first 10 bytes for a friendly string + // representation of the key. + FmtShort() string + // Express the PublicKey as a byte array + ToBytes() []byte +} + +// A public key. +// +// The key itself is just a 32 byte array, but a key has associated crypto +// information that is cached for performance reasons. +type PublicKeyOld struct { + ffiObject FfiObject +} + +// Make a PublicKey from byte array +func PublicKeyOldFromBytes(bytes []byte) (*PublicKeyOld, error) { + _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_publickeyold_from_bytes(FfiConverterBytesINSTANCE.Lower(bytes), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *PublicKeyOld + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterPublicKeyOldINSTANCE.Lift(_uniffiRV), nil + } +} + +// Make a PublicKey from base32 string +func PublicKeyOldFromString(s string) (*PublicKeyOld, error) { + _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_constructor_publickeyold_from_string(FfiConverterStringINSTANCE.Lower(s), _uniffiStatus) + }) + if _uniffiErr != nil { + var _uniffiDefaultValue *PublicKeyOld + return _uniffiDefaultValue, _uniffiErr + } else { + return FfiConverterPublicKeyOldINSTANCE.Lift(_uniffiRV), nil + } +} + +// Returns true if the PublicKeys are equal +func (_self *PublicKeyOld) Equal(other *PublicKeyOld) bool { + _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") + defer _self.ffiObject.decrementPointer() + return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { + return C.uniffi_iroh_streamplace_fn_method_publickeyold_equal( + _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(other), _uniffiStatus) + })) +} + +// Convert to a base32 string limited to the first 10 bytes for a friendly string +// representation of the key. +func (_self *PublicKeyOld) FmtShort() string { + _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") + defer _self.ffiObject.decrementPointer() + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_fmt_short( + _pointer, _uniffiStatus), + } + })) +} + +// Express the PublicKey as a byte array +func (_self *PublicKeyOld) ToBytes() []byte { + _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") + defer _self.ffiObject.decrementPointer() + return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_to_bytes( + _pointer, _uniffiStatus), + } + })) +} + +func (_self *PublicKeyOld) String() string { + _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") + defer _self.ffiObject.decrementPointer() + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_uniffi_trait_display( + _pointer, _uniffiStatus), + } + })) +} + +func (object *PublicKeyOld) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterPublicKeyOld struct{} + +var FfiConverterPublicKeyOldINSTANCE = FfiConverterPublicKeyOld{} + +func (c FfiConverterPublicKeyOld) Lift(pointer unsafe.Pointer) *PublicKeyOld { + result := &PublicKeyOld{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_publickeyold(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_publickeyold(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*PublicKeyOld).Destroy) + return result +} + +func (c FfiConverterPublicKeyOld) Read(reader io.Reader) *PublicKeyOld { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterPublicKeyOld) Lower(value *PublicKeyOld) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*PublicKeyOld") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterPublicKeyOld) Write(writer io.Writer, value *PublicKeyOld) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerPublicKeyOld struct{} + +func (_ FfiDestroyerPublicKeyOld) Destroy(value *PublicKeyOld) { + value.Destroy() +} + +type ReceiverInterface interface { + NodeAddr() *NodeAddr + // Subscribe to the given topic on the remote. + Subscribe(remoteId *PublicKeyOld, topic string) error + // Unsubscribe from this topic on the remote. + Unsubscribe(remoteId *PublicKeyOld, topic string) error +} +type Receiver struct { + ffiObject FfiObject +} + +// Create a new receiver. +func NewReceiver(endpoint *Endpoint, handler DataHandlerOld) (*Receiver, error) { + res, err := uniffiRustCallAsync[Error]( + FfiConverterErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *Receiver { + return FfiConverterReceiverINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_constructor_receiver_new(FfiConverterEndpointINSTANCE.Lower(endpoint), FfiConverterDataHandlerOldINSTANCE.Lower(handler)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res, err +} + +func (_self *Receiver) NodeAddr() *NodeAddr { + _pointer := _self.ffiObject.incrementPointer("*Receiver") + defer _self.ffiObject.decrementPointer() + res, _ := uniffiRustCallAsync[error]( + nil, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *NodeAddr { + return FfiConverterNodeAddrINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_receiver_node_addr( + _pointer), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res +} + +// Subscribe to the given topic on the remote. +func (_self *Receiver) Subscribe(remoteId *PublicKeyOld, topic string) error { + _pointer := _self.ffiObject.incrementPointer("*Receiver") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[Error]( + FfiConverterErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_receiver_subscribe( + _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} + +// Unsubscribe from this topic on the remote. +func (_self *Receiver) Unsubscribe(remoteId *PublicKeyOld, topic string) error { + _pointer := _self.ffiObject.incrementPointer("*Receiver") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[Error]( + FfiConverterErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_receiver_unsubscribe( + _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} +func (object *Receiver) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterReceiver struct{} + +var FfiConverterReceiverINSTANCE = FfiConverterReceiver{} + +func (c FfiConverterReceiver) Lift(pointer unsafe.Pointer) *Receiver { + result := &Receiver{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_receiver(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_receiver(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*Receiver).Destroy) + return result +} + +func (c FfiConverterReceiver) Read(reader io.Reader) *Receiver { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterReceiver) Lower(value *Receiver) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Receiver") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterReceiver) Write(writer io.Writer, value *Receiver) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerReceiver struct{} + +func (_ FfiDestroyerReceiver) Destroy(value *Receiver) { + value.Destroy() +} + +type SenderInterface interface { + NodeAddr() *NodeAddr + // Sends the given data to all subscribers that have subscribed to this `key`. + Send(key string, data []byte) error +} +type Sender struct { + ffiObject FfiObject +} + +// Create a new sender. +func NewSender(endpoint *Endpoint) (*Sender, error) { + res, err := uniffiRustCallAsync[Error]( + FfiConverterErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *Sender { + return FfiConverterSenderINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_constructor_sender_new(FfiConverterEndpointINSTANCE.Lower(endpoint)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res, err +} + +func (_self *Sender) NodeAddr() *NodeAddr { + _pointer := _self.ffiObject.incrementPointer("*Sender") + defer _self.ffiObject.decrementPointer() + res, _ := uniffiRustCallAsync[error]( + nil, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { + res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) + return res + }, + // liftFn + func(ffi unsafe.Pointer) *NodeAddr { + return FfiConverterNodeAddrINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_sender_node_addr( + _pointer), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_pointer(handle) + }, + ) + + return res +} + +// Sends the given data to all subscribers that have subscribed to this `key`. +func (_self *Sender) Send(key string, data []byte) error { + _pointer := _self.ffiObject.incrementPointer("*Sender") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[Error]( + FfiConverterErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_sender_send( + _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterBytesINSTANCE.Lower(data)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} +func (object *Sender) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterSender struct{} + +var FfiConverterSenderINSTANCE = FfiConverterSender{} + +func (c FfiConverterSender) Lift(pointer unsafe.Pointer) *Sender { + result := &Sender{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_sender(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_sender(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*Sender).Destroy) + return result +} + +func (c FfiConverterSender) Read(reader io.Reader) *Sender { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterSender) Lower(value *Sender) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*Sender") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterSender) Write(writer io.Writer, value *Sender) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerSender struct{} + +func (_ FfiDestroyerSender) Destroy(value *Sender) { + value.Destroy() +} + +// A response to a subscribe request. +// +// This can be used as a stream of [`SubscribeItem`]s. +type SubscribeResponseInterface interface { + NextRaw() (*SubscribeItem, error) +} + +// A response to a subscribe request. +// +// This can be used as a stream of [`SubscribeItem`]s. +type SubscribeResponse struct { + ffiObject FfiObject +} + +func (_self *SubscribeResponse) NextRaw() (*SubscribeItem, error) { + _pointer := _self.ffiObject.incrementPointer("*SubscribeResponse") + defer _self.ffiObject.decrementPointer() + res, err := uniffiRustCallAsync[SubscribeNextError]( + FfiConverterSubscribeNextErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) RustBufferI { + res := C.ffi_iroh_streamplace_rust_future_complete_rust_buffer(handle, status) + return GoRustBuffer{ + inner: res, + } + }, + // liftFn + func(ffi RustBufferI) *SubscribeItem { + return FfiConverterOptionalSubscribeItemINSTANCE.Lift(ffi) + }, + C.uniffi_iroh_streamplace_fn_method_subscriberesponse_next_raw( + _pointer), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_rust_buffer(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_rust_buffer(handle) + }, + ) + + return res, err +} + +func (_self *SubscribeResponse) DebugString() string { + _pointer := _self.ffiObject.incrementPointer("*SubscribeResponse") + defer _self.ffiObject.decrementPointer() + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_method_subscriberesponse_uniffi_trait_debug( + _pointer, _uniffiStatus), + } + })) +} + +func (object *SubscribeResponse) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterSubscribeResponse struct{} + +var FfiConverterSubscribeResponseINSTANCE = FfiConverterSubscribeResponse{} + +func (c FfiConverterSubscribeResponse) Lift(pointer unsafe.Pointer) *SubscribeResponse { + result := &SubscribeResponse{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_subscriberesponse(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_subscriberesponse(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*SubscribeResponse).Destroy) + return result +} + +func (c FfiConverterSubscribeResponse) Read(reader io.Reader) *SubscribeResponse { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterSubscribeResponse) Lower(value *SubscribeResponse) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*SubscribeResponse") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterSubscribeResponse) Write(writer io.Writer, value *SubscribeResponse) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerSubscribeResponse struct{} + +func (_ FfiDestroyerSubscribeResponse) Destroy(value *SubscribeResponse) { + value.Destroy() +} + +// A write scope that can be used to put values into the database. +// +// The default write scope is available from the [`Node::node_scope`] method. +type WriteScopeInterface interface { + Put(stream *[]byte, key []byte, value []byte) error +} + +// A write scope that can be used to put values into the database. +// +// The default write scope is available from the [`Node::node_scope`] method. +type WriteScope struct { + ffiObject FfiObject +} + +func (_self *WriteScope) Put(stream *[]byte, key []byte, value []byte) error { + _pointer := _self.ffiObject.incrementPointer("*WriteScope") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[PutError]( + FfiConverterPutErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_writescope_put( + _pointer, FfiConverterOptionalBytesINSTANCE.Lower(stream), FfiConverterBytesINSTANCE.Lower(key), FfiConverterBytesINSTANCE.Lower(value)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + return err +} +func (object *WriteScope) Destroy() { + runtime.SetFinalizer(object, nil) + object.ffiObject.destroy() +} + +type FfiConverterWriteScope struct{} + +var FfiConverterWriteScopeINSTANCE = FfiConverterWriteScope{} + +func (c FfiConverterWriteScope) Lift(pointer unsafe.Pointer) *WriteScope { + result := &WriteScope{ + newFfiObject( + pointer, + func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { + return C.uniffi_iroh_streamplace_fn_clone_writescope(pointer, status) + }, + func(pointer unsafe.Pointer, status *C.RustCallStatus) { + C.uniffi_iroh_streamplace_fn_free_writescope(pointer, status) + }, + ), + } + runtime.SetFinalizer(result, (*WriteScope).Destroy) + return result +} + +func (c FfiConverterWriteScope) Read(reader io.Reader) *WriteScope { + return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +} + +func (c FfiConverterWriteScope) Lower(value *WriteScope) unsafe.Pointer { + // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, + // because the pointer will be decremented immediately after this function returns, + // and someone will be left holding onto a non-locked pointer. + pointer := value.ffiObject.incrementPointer("*WriteScope") + defer value.ffiObject.decrementPointer() + return pointer + +} + +func (c FfiConverterWriteScope) Write(writer io.Writer, value *WriteScope) { + writeUint64(writer, uint64(uintptr(c.Lower(value)))) +} + +type FfiDestroyerWriteScope struct{} + +func (_ FfiDestroyerWriteScope) Destroy(value *WriteScope) { + value.Destroy() +} + +// Configuration for an iroh-streamplace node. +type Config struct { + // An Ed25519 secret key as a 32 byte array. + Key []byte + // The gossip topic to use. Must be 32 bytes. + // + // You can use e.g. a BLAKE3 hash of a topic string here. This can be used + // as a cheap way to have a shared secret - nodes that do not know the topic + // cannot connect to the swarm. + Topic []byte + // Maximum duration to wait for sending a stream piece to a peer. + MaxSendDuration time.Duration + // Disable using relays, for tests. + DisableRelay bool +} + +func (r *Config) Destroy() { + FfiDestroyerBytes{}.Destroy(r.Key) + FfiDestroyerBytes{}.Destroy(r.Topic) + FfiDestroyerDuration{}.Destroy(r.MaxSendDuration) + FfiDestroyerBool{}.Destroy(r.DisableRelay) +} + +type FfiConverterConfig struct{} + +var FfiConverterConfigINSTANCE = FfiConverterConfig{} + +func (c FfiConverterConfig) Lift(rb RustBufferI) Config { + return LiftFromRustBuffer[Config](c, rb) +} + +func (c FfiConverterConfig) Read(reader io.Reader) Config { + return Config{ + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterDurationINSTANCE.Read(reader), + FfiConverterBoolINSTANCE.Read(reader), + } +} + +func (c FfiConverterConfig) Lower(value Config) C.RustBuffer { + return LowerIntoRustBuffer[Config](c, value) +} + +func (c FfiConverterConfig) Write(writer io.Writer, value Config) { + FfiConverterBytesINSTANCE.Write(writer, value.Key) + FfiConverterBytesINSTANCE.Write(writer, value.Topic) + FfiConverterDurationINSTANCE.Write(writer, value.MaxSendDuration) + FfiConverterBoolINSTANCE.Write(writer, value.DisableRelay) +} + +type FfiDestroyerConfig struct{} + +func (_ FfiDestroyerConfig) Destroy(value Config) { + value.Destroy() +} + +// An entry returned from the database. +type Entry struct { + Scope *PublicKey + Stream *[]byte + Key []byte + Value []byte + Timestamp uint64 +} + +func (r *Entry) Destroy() { + FfiDestroyerPublicKey{}.Destroy(r.Scope) + FfiDestroyerOptionalBytes{}.Destroy(r.Stream) + FfiDestroyerBytes{}.Destroy(r.Key) + FfiDestroyerBytes{}.Destroy(r.Value) + FfiDestroyerUint64{}.Destroy(r.Timestamp) +} + +type FfiConverterEntry struct{} + +var FfiConverterEntryINSTANCE = FfiConverterEntry{} + +func (c FfiConverterEntry) Lift(rb RustBufferI) Entry { + return LiftFromRustBuffer[Entry](c, rb) +} + +func (c FfiConverterEntry) Read(reader io.Reader) Entry { + return Entry{ + FfiConverterPublicKeyINSTANCE.Read(reader), + FfiConverterOptionalBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterEntry) Lower(value Entry) C.RustBuffer { + return LowerIntoRustBuffer[Entry](c, value) +} + +func (c FfiConverterEntry) Write(writer io.Writer, value Entry) { + FfiConverterPublicKeyINSTANCE.Write(writer, value.Scope) + FfiConverterOptionalBytesINSTANCE.Write(writer, value.Stream) + FfiConverterBytesINSTANCE.Write(writer, value.Key) + FfiConverterBytesINSTANCE.Write(writer, value.Value) + FfiConverterUint64INSTANCE.Write(writer, value.Timestamp) +} + +type FfiDestroyerEntry struct{} + +func (_ FfiDestroyerEntry) Destroy(value Entry) { + value.Destroy() +} + +// Options for subscribing. +// +// `filter` specifies what to subscribe to. +// `mode` specifies whether to get current items, new items, or both. +type SubscribeOpts struct { + Filter *Filter + Mode SubscribeMode +} + +func (r *SubscribeOpts) Destroy() { + FfiDestroyerFilter{}.Destroy(r.Filter) + FfiDestroyerSubscribeMode{}.Destroy(r.Mode) +} + +type FfiConverterSubscribeOpts struct{} + +var FfiConverterSubscribeOptsINSTANCE = FfiConverterSubscribeOpts{} + +func (c FfiConverterSubscribeOpts) Lift(rb RustBufferI) SubscribeOpts { + return LiftFromRustBuffer[SubscribeOpts](c, rb) +} + +func (c FfiConverterSubscribeOpts) Read(reader io.Reader) SubscribeOpts { + return SubscribeOpts{ + FfiConverterFilterINSTANCE.Read(reader), + FfiConverterSubscribeModeINSTANCE.Read(reader), + } +} + +func (c FfiConverterSubscribeOpts) Lower(value SubscribeOpts) C.RustBuffer { + return LowerIntoRustBuffer[SubscribeOpts](c, value) +} + +func (c FfiConverterSubscribeOpts) Write(writer io.Writer, value SubscribeOpts) { + FfiConverterFilterINSTANCE.Write(writer, value.Filter) + FfiConverterSubscribeModeINSTANCE.Write(writer, value.Mode) +} + +type FfiDestroyerSubscribeOpts struct{} + +func (_ FfiDestroyerSubscribeOpts) Destroy(value SubscribeOpts) { + value.Destroy() +} + +// Error creating a new database node. +type CreateError struct { + err error +} + +// Convience method to turn *CreateError into error +// Avoiding treating nil pointer as non nil error interface +func (err *CreateError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err CreateError) Error() string { + return fmt.Sprintf("CreateError: %s", err.err.Error()) +} + +func (err CreateError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrCreateErrorPrivateKey = fmt.Errorf("CreateErrorPrivateKey") +var ErrCreateErrorTopic = fmt.Errorf("CreateErrorTopic") +var ErrCreateErrorBind = fmt.Errorf("CreateErrorBind") +var ErrCreateErrorSubscribe = fmt.Errorf("CreateErrorSubscribe") + +// Variant structs +// The provided private key is invalid (not 32 bytes). +type CreateErrorPrivateKey struct { + Size uint64 +} + +// The provided private key is invalid (not 32 bytes). +func NewCreateErrorPrivateKey( + size uint64, +) *CreateError { + return &CreateError{err: &CreateErrorPrivateKey{ + Size: size}} +} + +func (e CreateErrorPrivateKey) destroy() { + FfiDestroyerUint64{}.Destroy(e.Size) +} + +func (err CreateErrorPrivateKey) Error() string { + return fmt.Sprint("PrivateKey", + ": ", + + "Size=", + err.Size, + ) +} + +func (self CreateErrorPrivateKey) Is(target error) bool { + return target == ErrCreateErrorPrivateKey +} + +// The provided gossip topic is invalid (not 32 bytes). +type CreateErrorTopic struct { + Size uint64 +} + +// The provided gossip topic is invalid (not 32 bytes). +func NewCreateErrorTopic( + size uint64, +) *CreateError { + return &CreateError{err: &CreateErrorTopic{ + Size: size}} +} + +func (e CreateErrorTopic) destroy() { + FfiDestroyerUint64{}.Destroy(e.Size) +} + +func (err CreateErrorTopic) Error() string { + return fmt.Sprint("Topic", + ": ", + + "Size=", + err.Size, + ) +} + +func (self CreateErrorTopic) Is(target error) bool { + return target == ErrCreateErrorTopic +} + +// Failed to bind the iroh endpoint. +type CreateErrorBind struct { + Message string +} + +// Failed to bind the iroh endpoint. +func NewCreateErrorBind( + message string, +) *CreateError { + return &CreateError{err: &CreateErrorBind{ + Message: message}} +} + +func (e CreateErrorBind) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err CreateErrorBind) Error() string { + return fmt.Sprint("Bind", + ": ", + + "Message=", + err.Message, + ) +} + +func (self CreateErrorBind) Is(target error) bool { + return target == ErrCreateErrorBind +} + +// Failed to subscribe to the gossip topic. +type CreateErrorSubscribe struct { + Message string +} + +// Failed to subscribe to the gossip topic. +func NewCreateErrorSubscribe( + message string, +) *CreateError { + return &CreateError{err: &CreateErrorSubscribe{ + Message: message}} +} + +func (e CreateErrorSubscribe) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err CreateErrorSubscribe) Error() string { + return fmt.Sprint("Subscribe", + ": ", + + "Message=", + err.Message, + ) +} + +func (self CreateErrorSubscribe) Is(target error) bool { + return target == ErrCreateErrorSubscribe +} + +type FfiConverterCreateError struct{} + +var FfiConverterCreateErrorINSTANCE = FfiConverterCreateError{} + +func (c FfiConverterCreateError) Lift(eb RustBufferI) *CreateError { + return LiftFromRustBuffer[*CreateError](c, eb) +} + +func (c FfiConverterCreateError) Lower(value *CreateError) C.RustBuffer { + return LowerIntoRustBuffer[*CreateError](c, value) +} + +func (c FfiConverterCreateError) Read(reader io.Reader) *CreateError { + errorID := readUint32(reader) + + switch errorID { + case 1: + return &CreateError{&CreateErrorPrivateKey{ + Size: FfiConverterUint64INSTANCE.Read(reader), + }} + case 2: + return &CreateError{&CreateErrorTopic{ + Size: FfiConverterUint64INSTANCE.Read(reader), + }} + case 3: + return &CreateError{&CreateErrorBind{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + case 4: + return &CreateError{&CreateErrorSubscribe{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterCreateError.Read()", errorID)) + } +} + +func (c FfiConverterCreateError) Write(writer io.Writer, value *CreateError) { + switch variantValue := value.err.(type) { + case *CreateErrorPrivateKey: + writeInt32(writer, 1) + FfiConverterUint64INSTANCE.Write(writer, variantValue.Size) + case *CreateErrorTopic: + writeInt32(writer, 2) + FfiConverterUint64INSTANCE.Write(writer, variantValue.Size) + case *CreateErrorBind: + writeInt32(writer, 3) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + case *CreateErrorSubscribe: + writeInt32(writer, 4) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterCreateError.Write", value)) + } +} + +type FfiDestroyerCreateError struct{} + +func (_ FfiDestroyerCreateError) Destroy(value *CreateError) { + switch variantValue := value.err.(type) { + case CreateErrorPrivateKey: + variantValue.destroy() + case CreateErrorTopic: + variantValue.destroy() + case CreateErrorBind: + variantValue.destroy() + case CreateErrorSubscribe: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerCreateError.Destroy", value)) + } +} + +// An Error. +type Error struct { + err error +} + +// Convience method to turn *Error into error +// Avoiding treating nil pointer as non nil error interface +func (err *Error) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err Error) Error() string { + return fmt.Sprintf("Error: %s", err.err.Error()) +} + +func (err Error) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrErrorIrohBind = fmt.Errorf("ErrorIrohBind") +var ErrErrorInvalidUrl = fmt.Errorf("ErrorInvalidUrl") +var ErrErrorIrohConnect = fmt.Errorf("ErrorIrohConnect") +var ErrErrorInvalidNetworkAddress = fmt.Errorf("ErrorInvalidNetworkAddress") +var ErrErrorMissingConnection = fmt.Errorf("ErrorMissingConnection") +var ErrErrorInvalidPublicKey = fmt.Errorf("ErrorInvalidPublicKey") +var ErrErrorIrpc = fmt.Errorf("ErrorIrpc") + +// Variant structs +type ErrorIrohBind struct { + message string +} + +func NewErrorIrohBind() *Error { + return &Error{err: &ErrorIrohBind{}} +} + +func (e ErrorIrohBind) destroy() { +} + +func (err ErrorIrohBind) Error() string { + return fmt.Sprintf("IrohBind: %s", err.message) +} + +func (self ErrorIrohBind) Is(target error) bool { + return target == ErrErrorIrohBind +} + +type ErrorInvalidUrl struct { + message string +} + +func NewErrorInvalidUrl() *Error { + return &Error{err: &ErrorInvalidUrl{}} +} + +func (e ErrorInvalidUrl) destroy() { +} + +func (err ErrorInvalidUrl) Error() string { + return fmt.Sprintf("InvalidUrl: %s", err.message) +} + +func (self ErrorInvalidUrl) Is(target error) bool { + return target == ErrErrorInvalidUrl +} + +type ErrorIrohConnect struct { + message string +} + +func NewErrorIrohConnect() *Error { + return &Error{err: &ErrorIrohConnect{}} +} + +func (e ErrorIrohConnect) destroy() { +} + +func (err ErrorIrohConnect) Error() string { + return fmt.Sprintf("IrohConnect: %s", err.message) +} + +func (self ErrorIrohConnect) Is(target error) bool { + return target == ErrErrorIrohConnect +} + +type ErrorInvalidNetworkAddress struct { + message string +} + +func NewErrorInvalidNetworkAddress() *Error { + return &Error{err: &ErrorInvalidNetworkAddress{}} +} + +func (e ErrorInvalidNetworkAddress) destroy() { +} + +func (err ErrorInvalidNetworkAddress) Error() string { + return fmt.Sprintf("InvalidNetworkAddress: %s", err.message) +} + +func (self ErrorInvalidNetworkAddress) Is(target error) bool { + return target == ErrErrorInvalidNetworkAddress +} + +type ErrorMissingConnection struct { + message string +} + +func NewErrorMissingConnection() *Error { + return &Error{err: &ErrorMissingConnection{}} +} + +func (e ErrorMissingConnection) destroy() { +} + +func (err ErrorMissingConnection) Error() string { + return fmt.Sprintf("MissingConnection: %s", err.message) +} + +func (self ErrorMissingConnection) Is(target error) bool { + return target == ErrErrorMissingConnection +} + +type ErrorInvalidPublicKey struct { + message string +} + +func NewErrorInvalidPublicKey() *Error { + return &Error{err: &ErrorInvalidPublicKey{}} +} + +func (e ErrorInvalidPublicKey) destroy() { +} + +func (err ErrorInvalidPublicKey) Error() string { + return fmt.Sprintf("InvalidPublicKey: %s", err.message) +} + +func (self ErrorInvalidPublicKey) Is(target error) bool { + return target == ErrErrorInvalidPublicKey +} + +type ErrorIrpc struct { + message string +} + +func NewErrorIrpc() *Error { + return &Error{err: &ErrorIrpc{}} +} + +func (e ErrorIrpc) destroy() { +} + +func (err ErrorIrpc) Error() string { + return fmt.Sprintf("Irpc: %s", err.message) +} + +func (self ErrorIrpc) Is(target error) bool { + return target == ErrErrorIrpc +} + +type FfiConverterError struct{} + +var FfiConverterErrorINSTANCE = FfiConverterError{} + +func (c FfiConverterError) Lift(eb RustBufferI) *Error { + return LiftFromRustBuffer[*Error](c, eb) +} + +func (c FfiConverterError) Lower(value *Error) C.RustBuffer { + return LowerIntoRustBuffer[*Error](c, value) +} + +func (c FfiConverterError) Read(reader io.Reader) *Error { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &Error{&ErrorIrohBind{message}} + case 2: + return &Error{&ErrorInvalidUrl{message}} + case 3: + return &Error{&ErrorIrohConnect{message}} + case 4: + return &Error{&ErrorInvalidNetworkAddress{message}} + case 5: + return &Error{&ErrorMissingConnection{message}} + case 6: + return &Error{&ErrorInvalidPublicKey{message}} + case 7: + return &Error{&ErrorIrpc{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterError.Read()", errorID)) + } + +} + +func (c FfiConverterError) Write(writer io.Writer, value *Error) { + switch variantValue := value.err.(type) { + case *ErrorIrohBind: + writeInt32(writer, 1) + case *ErrorInvalidUrl: + writeInt32(writer, 2) + case *ErrorIrohConnect: + writeInt32(writer, 3) + case *ErrorInvalidNetworkAddress: + writeInt32(writer, 4) + case *ErrorMissingConnection: + writeInt32(writer, 5) + case *ErrorInvalidPublicKey: + writeInt32(writer, 6) + case *ErrorIrpc: + writeInt32(writer, 7) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterError.Write", value)) + } +} + +type FfiDestroyerError struct{} + +func (_ FfiDestroyerError) Destroy(value *Error) { + switch variantValue := value.err.(type) { + case ErrorIrohBind: + variantValue.destroy() + case ErrorInvalidUrl: + variantValue.destroy() + case ErrorIrohConnect: + variantValue.destroy() + case ErrorInvalidNetworkAddress: + variantValue.destroy() + case ErrorMissingConnection: + variantValue.destroy() + case ErrorInvalidPublicKey: + variantValue.destroy() + case ErrorIrpc: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerError.Destroy", value)) + } +} + +// Error joining peers. +type JoinPeersError struct { + err error +} + +// Convience method to turn *JoinPeersError into error +// Avoiding treating nil pointer as non nil error interface +func (err *JoinPeersError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err JoinPeersError) Error() string { + return fmt.Sprintf("JoinPeersError: %s", err.err.Error()) +} + +func (err JoinPeersError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrJoinPeersErrorTicket = fmt.Errorf("JoinPeersErrorTicket") +var ErrJoinPeersErrorIrpc = fmt.Errorf("JoinPeersErrorIrpc") + +// Variant structs +// Failed to parse a provided iroh node ticket. +type JoinPeersErrorTicket struct { + Message string +} + +// Failed to parse a provided iroh node ticket. +func NewJoinPeersErrorTicket( + message string, +) *JoinPeersError { + return &JoinPeersError{err: &JoinPeersErrorTicket{ + Message: message}} +} + +func (e JoinPeersErrorTicket) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err JoinPeersErrorTicket) Error() string { + return fmt.Sprint("Ticket", + ": ", + + "Message=", + err.Message, + ) +} + +func (self JoinPeersErrorTicket) Is(target error) bool { + return target == ErrJoinPeersErrorTicket +} + +// Error during the join peers operation. +type JoinPeersErrorIrpc struct { + Message string +} + +// Error during the join peers operation. +func NewJoinPeersErrorIrpc( + message string, +) *JoinPeersError { + return &JoinPeersError{err: &JoinPeersErrorIrpc{ + Message: message}} +} + +func (e JoinPeersErrorIrpc) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err JoinPeersErrorIrpc) Error() string { + return fmt.Sprint("Irpc", + ": ", + + "Message=", + err.Message, + ) +} + +func (self JoinPeersErrorIrpc) Is(target error) bool { + return target == ErrJoinPeersErrorIrpc +} + +type FfiConverterJoinPeersError struct{} + +var FfiConverterJoinPeersErrorINSTANCE = FfiConverterJoinPeersError{} + +func (c FfiConverterJoinPeersError) Lift(eb RustBufferI) *JoinPeersError { + return LiftFromRustBuffer[*JoinPeersError](c, eb) +} + +func (c FfiConverterJoinPeersError) Lower(value *JoinPeersError) C.RustBuffer { + return LowerIntoRustBuffer[*JoinPeersError](c, value) +} + +func (c FfiConverterJoinPeersError) Read(reader io.Reader) *JoinPeersError { + errorID := readUint32(reader) + + switch errorID { + case 1: + return &JoinPeersError{&JoinPeersErrorTicket{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + case 2: + return &JoinPeersError{&JoinPeersErrorIrpc{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterJoinPeersError.Read()", errorID)) + } +} + +func (c FfiConverterJoinPeersError) Write(writer io.Writer, value *JoinPeersError) { + switch variantValue := value.err.(type) { + case *JoinPeersErrorTicket: + writeInt32(writer, 1) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + case *JoinPeersErrorIrpc: + writeInt32(writer, 2) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterJoinPeersError.Write", value)) + } +} + +type FfiDestroyerJoinPeersError struct{} + +func (_ FfiDestroyerJoinPeersError) Destroy(value *JoinPeersError) { + switch variantValue := value.err.(type) { + case JoinPeersErrorTicket: + variantValue.destroy() + case JoinPeersErrorIrpc: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerJoinPeersError.Destroy", value)) + } +} + +type PublicKeyError struct { + err error +} + +// Convience method to turn *PublicKeyError into error +// Avoiding treating nil pointer as non nil error interface +func (err *PublicKeyError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err PublicKeyError) Error() string { + return fmt.Sprintf("PublicKeyError: %s", err.err.Error()) +} + +func (err PublicKeyError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrPublicKeyErrorLength = fmt.Errorf("PublicKeyErrorLength") +var ErrPublicKeyErrorInvalid = fmt.Errorf("PublicKeyErrorInvalid") + +// Variant structs +type PublicKeyErrorLength struct { + Size uint64 +} + +func NewPublicKeyErrorLength( + size uint64, +) *PublicKeyError { + return &PublicKeyError{err: &PublicKeyErrorLength{ + Size: size}} +} + +func (e PublicKeyErrorLength) destroy() { + FfiDestroyerUint64{}.Destroy(e.Size) +} + +func (err PublicKeyErrorLength) Error() string { + return fmt.Sprint("Length", + ": ", + + "Size=", + err.Size, + ) +} + +func (self PublicKeyErrorLength) Is(target error) bool { + return target == ErrPublicKeyErrorLength +} + +type PublicKeyErrorInvalid struct { + Message string +} + +func NewPublicKeyErrorInvalid( + message string, +) *PublicKeyError { + return &PublicKeyError{err: &PublicKeyErrorInvalid{ + Message: message}} +} + +func (e PublicKeyErrorInvalid) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err PublicKeyErrorInvalid) Error() string { + return fmt.Sprint("Invalid", + ": ", + + "Message=", + err.Message, + ) +} + +func (self PublicKeyErrorInvalid) Is(target error) bool { + return target == ErrPublicKeyErrorInvalid +} + +type FfiConverterPublicKeyError struct{} + +var FfiConverterPublicKeyErrorINSTANCE = FfiConverterPublicKeyError{} + +func (c FfiConverterPublicKeyError) Lift(eb RustBufferI) *PublicKeyError { + return LiftFromRustBuffer[*PublicKeyError](c, eb) +} + +func (c FfiConverterPublicKeyError) Lower(value *PublicKeyError) C.RustBuffer { + return LowerIntoRustBuffer[*PublicKeyError](c, value) +} + +func (c FfiConverterPublicKeyError) Read(reader io.Reader) *PublicKeyError { + errorID := readUint32(reader) + + switch errorID { + case 1: + return &PublicKeyError{&PublicKeyErrorLength{ + Size: FfiConverterUint64INSTANCE.Read(reader), + }} + case 2: + return &PublicKeyError{&PublicKeyErrorInvalid{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterPublicKeyError.Read()", errorID)) + } +} + +func (c FfiConverterPublicKeyError) Write(writer io.Writer, value *PublicKeyError) { + switch variantValue := value.err.(type) { + case *PublicKeyErrorLength: + writeInt32(writer, 1) + FfiConverterUint64INSTANCE.Write(writer, variantValue.Size) + case *PublicKeyErrorInvalid: + writeInt32(writer, 2) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterPublicKeyError.Write", value)) + } +} + +type FfiDestroyerPublicKeyError struct{} + +func (_ FfiDestroyerPublicKeyError) Destroy(value *PublicKeyError) { + switch variantValue := value.err.(type) { + case PublicKeyErrorLength: + variantValue.destroy() + case PublicKeyErrorInvalid: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerPublicKeyError.Destroy", value)) + } +} + +// Error putting a value into the database. +type PutError struct { + err error +} + +// Convience method to turn *PutError into error +// Avoiding treating nil pointer as non nil error interface +func (err *PutError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err PutError) Error() string { + return fmt.Sprintf("PutError: %s", err.err.Error()) +} + +func (err PutError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrPutErrorIrpc = fmt.Errorf("PutErrorIrpc") + +// Variant structs +// Error during the put operation. +type PutErrorIrpc struct { + Message string +} + +// Error during the put operation. +func NewPutErrorIrpc( + message string, +) *PutError { + return &PutError{err: &PutErrorIrpc{ + Message: message}} +} + +func (e PutErrorIrpc) destroy() { + FfiDestroyerString{}.Destroy(e.Message) +} + +func (err PutErrorIrpc) Error() string { + return fmt.Sprint("Irpc", + ": ", + + "Message=", + err.Message, + ) +} + +func (self PutErrorIrpc) Is(target error) bool { + return target == ErrPutErrorIrpc +} + +type FfiConverterPutError struct{} + +var FfiConverterPutErrorINSTANCE = FfiConverterPutError{} + +func (c FfiConverterPutError) Lift(eb RustBufferI) *PutError { + return LiftFromRustBuffer[*PutError](c, eb) +} + +func (c FfiConverterPutError) Lower(value *PutError) C.RustBuffer { + return LowerIntoRustBuffer[*PutError](c, value) +} + +func (c FfiConverterPutError) Read(reader io.Reader) *PutError { + errorID := readUint32(reader) + + switch errorID { + case 1: + return &PutError{&PutErrorIrpc{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterPutError.Read()", errorID)) + } +} + +func (c FfiConverterPutError) Write(writer io.Writer, value *PutError) { + switch variantValue := value.err.(type) { + case *PutErrorIrpc: + writeInt32(writer, 1) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterPutError.Write", value)) + } +} + +type FfiDestroyerPutError struct{} + +func (_ FfiDestroyerPutError) Destroy(value *PutError) { + switch variantValue := value.err.(type) { + case PutErrorIrpc: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerPutError.Destroy", value)) + } +} + +type SpError struct { + err error +} + +// Convience method to turn *SpError into error +// Avoiding treating nil pointer as non nil error interface +func (err *SpError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err SpError) Error() string { + return fmt.Sprintf("SpError: %s", err.err.Error()) +} + +func (err SpError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrSpErrorNoCertificateChainFound = fmt.Errorf("SpErrorNoCertificateChainFound") +var ErrSpErrorC2paError = fmt.Errorf("SpErrorC2paError") + +// Variant structs +type SpErrorNoCertificateChainFound struct { + message string +} + +func NewSpErrorNoCertificateChainFound() *SpError { + return &SpError{err: &SpErrorNoCertificateChainFound{}} +} + +func (e SpErrorNoCertificateChainFound) destroy() { +} + +func (err SpErrorNoCertificateChainFound) Error() string { + return fmt.Sprintf("NoCertificateChainFound: %s", err.message) +} + +func (self SpErrorNoCertificateChainFound) Is(target error) bool { + return target == ErrSpErrorNoCertificateChainFound +} + +type SpErrorC2paError struct { + message string +} + +func NewSpErrorC2paError() *SpError { + return &SpError{err: &SpErrorC2paError{}} +} + +func (e SpErrorC2paError) destroy() { } -// Sends the given data to all subscribers that have subscribed to this `key`. -func (_self *Sender) Send(key string, data []byte) *Error { - _pointer := _self.ffiObject.incrementPointer("*Sender") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_sender_send( - _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterBytesINSTANCE.Lower(data)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) +func (err SpErrorC2paError) Error() string { + return fmt.Sprintf("C2paError: %s", err.message) +} - return err +func (self SpErrorC2paError) Is(target error) bool { + return target == ErrSpErrorC2paError } -func (object *Sender) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() + +type FfiConverterSpError struct{} + +var FfiConverterSpErrorINSTANCE = FfiConverterSpError{} + +func (c FfiConverterSpError) Lift(eb RustBufferI) *SpError { + return LiftFromRustBuffer[*SpError](c, eb) } -type FfiConverterSender struct{} +func (c FfiConverterSpError) Lower(value *SpError) C.RustBuffer { + return LowerIntoRustBuffer[*SpError](c, value) +} -var FfiConverterSenderINSTANCE = FfiConverterSender{} +func (c FfiConverterSpError) Read(reader io.Reader) *SpError { + errorID := readUint32(reader) -func (c FfiConverterSender) Lift(pointer unsafe.Pointer) *Sender { - result := &Sender{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_sender(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_sender(pointer, status) - }, - ), + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &SpError{&SpErrorNoCertificateChainFound{message}} + case 2: + return &SpError{&SpErrorC2paError{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterSpError.Read()", errorID)) } - runtime.SetFinalizer(result, (*Sender).Destroy) - return result + } -func (c FfiConverterSender) Read(reader io.Reader) *Sender { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) +func (c FfiConverterSpError) Write(writer io.Writer, value *SpError) { + switch variantValue := value.err.(type) { + case *SpErrorNoCertificateChainFound: + writeInt32(writer, 1) + case *SpErrorC2paError: + writeInt32(writer, 2) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterSpError.Write", value)) + } } -func (c FfiConverterSender) Lower(value *Sender) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Sender") - defer value.ffiObject.decrementPointer() - return pointer +type FfiDestroyerSpError struct{} + +func (_ FfiDestroyerSpError) Destroy(value *SpError) { + switch variantValue := value.err.(type) { + case SpErrorNoCertificateChainFound: + variantValue.destroy() + case SpErrorC2paError: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerSpError.Destroy", value)) + } +} +type StreamFilter interface { + Destroy() +} +type StreamFilterAll struct { } -func (c FfiConverterSender) Write(writer io.Writer, value *Sender) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) +func (e StreamFilterAll) Destroy() { } -type FfiDestroyerSender struct{} +type StreamFilterGlobal struct { +} -func (_ FfiDestroyerSender) Destroy(value *Sender) { - value.Destroy() +func (e StreamFilterGlobal) Destroy() { } -// An Error. -type Error struct { - err error +type StreamFilterStream struct { + Field0 []byte } -// Convience method to turn *Error into error -// Avoiding treating nil pointer as non nil error interface -func (err *Error) AsError() error { - if err == nil { - return nil - } else { - return err +func (e StreamFilterStream) Destroy() { + FfiDestroyerBytes{}.Destroy(e.Field0) +} + +type FfiConverterStreamFilter struct{} + +var FfiConverterStreamFilterINSTANCE = FfiConverterStreamFilter{} + +func (c FfiConverterStreamFilter) Lift(rb RustBufferI) StreamFilter { + return LiftFromRustBuffer[StreamFilter](c, rb) +} + +func (c FfiConverterStreamFilter) Lower(value StreamFilter) C.RustBuffer { + return LowerIntoRustBuffer[StreamFilter](c, value) +} +func (FfiConverterStreamFilter) Read(reader io.Reader) StreamFilter { + id := readInt32(reader) + switch id { + case 1: + return StreamFilterAll{} + case 2: + return StreamFilterGlobal{} + case 3: + return StreamFilterStream{ + FfiConverterBytesINSTANCE.Read(reader), + } + default: + panic(fmt.Sprintf("invalid enum value %v in FfiConverterStreamFilter.Read()", id)) } } -func (err Error) Error() string { - return fmt.Sprintf("Error: %s", err.err.Error()) +func (FfiConverterStreamFilter) Write(writer io.Writer, value StreamFilter) { + switch variant_value := value.(type) { + case StreamFilterAll: + writeInt32(writer, 1) + case StreamFilterGlobal: + writeInt32(writer, 2) + case StreamFilterStream: + writeInt32(writer, 3) + FfiConverterBytesINSTANCE.Write(writer, variant_value.Field0) + default: + _ = variant_value + panic(fmt.Sprintf("invalid enum value `%v` in FfiConverterStreamFilter.Write", value)) + } } -func (err Error) Unwrap() error { - return err.err +type FfiDestroyerStreamFilter struct{} + +func (_ FfiDestroyerStreamFilter) Destroy(value StreamFilter) { + value.Destroy() } -// Err* are used for checking error type with `errors.Is` -var ErrErrorIrohBind = fmt.Errorf("ErrorIrohBind") -var ErrErrorInvalidUrl = fmt.Errorf("ErrorInvalidUrl") -var ErrErrorIrohConnect = fmt.Errorf("ErrorIrohConnect") -var ErrErrorInvalidNetworkAddress = fmt.Errorf("ErrorInvalidNetworkAddress") -var ErrErrorMissingConnection = fmt.Errorf("ErrorMissingConnection") -var ErrErrorInvalidPublicKey = fmt.Errorf("ErrorInvalidPublicKey") -var ErrErrorIrpc = fmt.Errorf("ErrorIrpc") +// An item returned from a subscription. +type SubscribeItem interface { + Destroy() +} +type SubscribeItemEntry struct { + Scope *PublicKey + Stream *[]byte + Key []byte + Value []byte + Timestamp uint64 +} -// Variant structs -type ErrorIrohBind struct { - message string +func (e SubscribeItemEntry) Destroy() { + FfiDestroyerPublicKey{}.Destroy(e.Scope) + FfiDestroyerOptionalBytes{}.Destroy(e.Stream) + FfiDestroyerBytes{}.Destroy(e.Key) + FfiDestroyerBytes{}.Destroy(e.Value) + FfiDestroyerUint64{}.Destroy(e.Timestamp) } -func NewErrorIrohBind() *Error { - return &Error{err: &ErrorIrohBind{}} +type SubscribeItemCurrentDone struct { } -func (e ErrorIrohBind) destroy() { +func (e SubscribeItemCurrentDone) Destroy() { } -func (err ErrorIrohBind) Error() string { - return fmt.Sprintf("IrohBind: %s", err.message) +type SubscribeItemExpired struct { + Scope *PublicKey + Stream *[]byte + Key []byte + Timestamp uint64 } -func (self ErrorIrohBind) Is(target error) bool { - return target == ErrErrorIrohBind +func (e SubscribeItemExpired) Destroy() { + FfiDestroyerPublicKey{}.Destroy(e.Scope) + FfiDestroyerOptionalBytes{}.Destroy(e.Stream) + FfiDestroyerBytes{}.Destroy(e.Key) + FfiDestroyerUint64{}.Destroy(e.Timestamp) } -type ErrorInvalidUrl struct { - message string +type SubscribeItemOther struct { } -func NewErrorInvalidUrl() *Error { - return &Error{err: &ErrorInvalidUrl{}} +func (e SubscribeItemOther) Destroy() { } -func (e ErrorInvalidUrl) destroy() { +type FfiConverterSubscribeItem struct{} + +var FfiConverterSubscribeItemINSTANCE = FfiConverterSubscribeItem{} + +func (c FfiConverterSubscribeItem) Lift(rb RustBufferI) SubscribeItem { + return LiftFromRustBuffer[SubscribeItem](c, rb) } -func (err ErrorInvalidUrl) Error() string { - return fmt.Sprintf("InvalidUrl: %s", err.message) +func (c FfiConverterSubscribeItem) Lower(value SubscribeItem) C.RustBuffer { + return LowerIntoRustBuffer[SubscribeItem](c, value) +} +func (FfiConverterSubscribeItem) Read(reader io.Reader) SubscribeItem { + id := readInt32(reader) + switch id { + case 1: + return SubscribeItemEntry{ + FfiConverterPublicKeyINSTANCE.Read(reader), + FfiConverterOptionalBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } + case 2: + return SubscribeItemCurrentDone{} + case 3: + return SubscribeItemExpired{ + FfiConverterPublicKeyINSTANCE.Read(reader), + FfiConverterOptionalBytesINSTANCE.Read(reader), + FfiConverterBytesINSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } + case 4: + return SubscribeItemOther{} + default: + panic(fmt.Sprintf("invalid enum value %v in FfiConverterSubscribeItem.Read()", id)) + } } -func (self ErrorInvalidUrl) Is(target error) bool { - return target == ErrErrorInvalidUrl +func (FfiConverterSubscribeItem) Write(writer io.Writer, value SubscribeItem) { + switch variant_value := value.(type) { + case SubscribeItemEntry: + writeInt32(writer, 1) + FfiConverterPublicKeyINSTANCE.Write(writer, variant_value.Scope) + FfiConverterOptionalBytesINSTANCE.Write(writer, variant_value.Stream) + FfiConverterBytesINSTANCE.Write(writer, variant_value.Key) + FfiConverterBytesINSTANCE.Write(writer, variant_value.Value) + FfiConverterUint64INSTANCE.Write(writer, variant_value.Timestamp) + case SubscribeItemCurrentDone: + writeInt32(writer, 2) + case SubscribeItemExpired: + writeInt32(writer, 3) + FfiConverterPublicKeyINSTANCE.Write(writer, variant_value.Scope) + FfiConverterOptionalBytesINSTANCE.Write(writer, variant_value.Stream) + FfiConverterBytesINSTANCE.Write(writer, variant_value.Key) + FfiConverterUint64INSTANCE.Write(writer, variant_value.Timestamp) + case SubscribeItemOther: + writeInt32(writer, 4) + default: + _ = variant_value + panic(fmt.Sprintf("invalid enum value `%v` in FfiConverterSubscribeItem.Write", value)) + } } -type ErrorIrohConnect struct { - message string +type FfiDestroyerSubscribeItem struct{} + +func (_ FfiDestroyerSubscribeItem) Destroy(value SubscribeItem) { + value.Destroy() } -func NewErrorIrohConnect() *Error { - return &Error{err: &ErrorIrohConnect{}} +// Subscription mode for key-value subscriptions. +type SubscribeMode uint + +const ( + SubscribeModeCurrent SubscribeMode = 1 + SubscribeModeFuture SubscribeMode = 2 + SubscribeModeBoth SubscribeMode = 3 +) + +type FfiConverterSubscribeMode struct{} + +var FfiConverterSubscribeModeINSTANCE = FfiConverterSubscribeMode{} + +func (c FfiConverterSubscribeMode) Lift(rb RustBufferI) SubscribeMode { + return LiftFromRustBuffer[SubscribeMode](c, rb) } -func (e ErrorIrohConnect) destroy() { +func (c FfiConverterSubscribeMode) Lower(value SubscribeMode) C.RustBuffer { + return LowerIntoRustBuffer[SubscribeMode](c, value) +} +func (FfiConverterSubscribeMode) Read(reader io.Reader) SubscribeMode { + id := readInt32(reader) + return SubscribeMode(id) } -func (err ErrorIrohConnect) Error() string { - return fmt.Sprintf("IrohConnect: %s", err.message) +func (FfiConverterSubscribeMode) Write(writer io.Writer, value SubscribeMode) { + writeInt32(writer, int32(value)) } -func (self ErrorIrohConnect) Is(target error) bool { - return target == ErrErrorIrohConnect +type FfiDestroyerSubscribeMode struct{} + +func (_ FfiDestroyerSubscribeMode) Destroy(value SubscribeMode) { } -type ErrorInvalidNetworkAddress struct { - message string +// Error getting the next item from a subscription. +type SubscribeNextError struct { + err error } -func NewErrorInvalidNetworkAddress() *Error { - return &Error{err: &ErrorInvalidNetworkAddress{}} +// Convience method to turn *SubscribeNextError into error +// Avoiding treating nil pointer as non nil error interface +func (err *SubscribeNextError) AsError() error { + if err == nil { + return nil + } else { + return err + } } -func (e ErrorInvalidNetworkAddress) destroy() { +func (err SubscribeNextError) Error() string { + return fmt.Sprintf("SubscribeNextError: %s", err.err.Error()) } -func (err ErrorInvalidNetworkAddress) Error() string { - return fmt.Sprintf("InvalidNetworkAddress: %s", err.message) +func (err SubscribeNextError) Unwrap() error { + return err.err } -func (self ErrorInvalidNetworkAddress) Is(target error) bool { - return target == ErrErrorInvalidNetworkAddress +// Err* are used for checking error type with `errors.Is` +var ErrSubscribeNextErrorIrpc = fmt.Errorf("SubscribeNextErrorIrpc") + +// Variant structs +// Error during the subscribe next operation. +type SubscribeNextErrorIrpc struct { + Message string } -type ErrorMissingConnection struct { - message string +// Error during the subscribe next operation. +func NewSubscribeNextErrorIrpc( + message string, +) *SubscribeNextError { + return &SubscribeNextError{err: &SubscribeNextErrorIrpc{ + Message: message}} } -func NewErrorMissingConnection() *Error { - return &Error{err: &ErrorMissingConnection{}} +func (e SubscribeNextErrorIrpc) destroy() { + FfiDestroyerString{}.Destroy(e.Message) } -func (e ErrorMissingConnection) destroy() { +func (err SubscribeNextErrorIrpc) Error() string { + return fmt.Sprint("Irpc", + ": ", + + "Message=", + err.Message, + ) } -func (err ErrorMissingConnection) Error() string { - return fmt.Sprintf("MissingConnection: %s", err.message) +func (self SubscribeNextErrorIrpc) Is(target error) bool { + return target == ErrSubscribeNextErrorIrpc } -func (self ErrorMissingConnection) Is(target error) bool { - return target == ErrErrorMissingConnection +type FfiConverterSubscribeNextError struct{} + +var FfiConverterSubscribeNextErrorINSTANCE = FfiConverterSubscribeNextError{} + +func (c FfiConverterSubscribeNextError) Lift(eb RustBufferI) *SubscribeNextError { + return LiftFromRustBuffer[*SubscribeNextError](c, eb) } -type ErrorInvalidPublicKey struct { - message string +func (c FfiConverterSubscribeNextError) Lower(value *SubscribeNextError) C.RustBuffer { + return LowerIntoRustBuffer[*SubscribeNextError](c, value) } -func NewErrorInvalidPublicKey() *Error { - return &Error{err: &ErrorInvalidPublicKey{}} +func (c FfiConverterSubscribeNextError) Read(reader io.Reader) *SubscribeNextError { + errorID := readUint32(reader) + + switch errorID { + case 1: + return &SubscribeNextError{&SubscribeNextErrorIrpc{ + Message: FfiConverterStringINSTANCE.Read(reader), + }} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterSubscribeNextError.Read()", errorID)) + } +} + +func (c FfiConverterSubscribeNextError) Write(writer io.Writer, value *SubscribeNextError) { + switch variantValue := value.err.(type) { + case *SubscribeNextErrorIrpc: + writeInt32(writer, 1) + FfiConverterStringINSTANCE.Write(writer, variantValue.Message) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterSubscribeNextError.Write", value)) + } } -func (e ErrorInvalidPublicKey) destroy() { -} +type FfiDestroyerSubscribeNextError struct{} -func (err ErrorInvalidPublicKey) Error() string { - return fmt.Sprintf("InvalidPublicKey: %s", err.message) +func (_ FfiDestroyerSubscribeNextError) Destroy(value *SubscribeNextError) { + switch variantValue := value.err.(type) { + case SubscribeNextErrorIrpc: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerSubscribeNextError.Destroy", value)) + } } -func (self ErrorInvalidPublicKey) Is(target error) bool { - return target == ErrErrorInvalidPublicKey +// A bound on time for filtering. +type TimeBound interface { + Destroy() +} +type TimeBoundUnbounded struct { } -type ErrorIrpc struct { - message string +func (e TimeBoundUnbounded) Destroy() { } -func NewErrorIrpc() *Error { - return &Error{err: &ErrorIrpc{}} +type TimeBoundIncluded struct { + Field0 uint64 } -func (e ErrorIrpc) destroy() { +func (e TimeBoundIncluded) Destroy() { + FfiDestroyerUint64{}.Destroy(e.Field0) } -func (err ErrorIrpc) Error() string { - return fmt.Sprintf("Irpc: %s", err.message) +type TimeBoundExcluded struct { + Field0 uint64 } -func (self ErrorIrpc) Is(target error) bool { - return target == ErrErrorIrpc +func (e TimeBoundExcluded) Destroy() { + FfiDestroyerUint64{}.Destroy(e.Field0) } -type FfiConverterError struct{} +type FfiConverterTimeBound struct{} -var FfiConverterErrorINSTANCE = FfiConverterError{} +var FfiConverterTimeBoundINSTANCE = FfiConverterTimeBound{} -func (c FfiConverterError) Lift(eb RustBufferI) *Error { - return LiftFromRustBuffer[*Error](c, eb) +func (c FfiConverterTimeBound) Lift(rb RustBufferI) TimeBound { + return LiftFromRustBuffer[TimeBound](c, rb) } -func (c FfiConverterError) Lower(value *Error) C.RustBuffer { - return LowerIntoRustBuffer[*Error](c, value) +func (c FfiConverterTimeBound) Lower(value TimeBound) C.RustBuffer { + return LowerIntoRustBuffer[TimeBound](c, value) } - -func (c FfiConverterError) Read(reader io.Reader) *Error { - errorID := readUint32(reader) - - message := FfiConverterStringINSTANCE.Read(reader) - switch errorID { +func (FfiConverterTimeBound) Read(reader io.Reader) TimeBound { + id := readInt32(reader) + switch id { case 1: - return &Error{&ErrorIrohBind{message}} + return TimeBoundUnbounded{} case 2: - return &Error{&ErrorInvalidUrl{message}} + return TimeBoundIncluded{ + FfiConverterUint64INSTANCE.Read(reader), + } case 3: - return &Error{&ErrorIrohConnect{message}} - case 4: - return &Error{&ErrorInvalidNetworkAddress{message}} - case 5: - return &Error{&ErrorMissingConnection{message}} - case 6: - return &Error{&ErrorInvalidPublicKey{message}} - case 7: - return &Error{&ErrorIrpc{message}} + return TimeBoundExcluded{ + FfiConverterUint64INSTANCE.Read(reader), + } default: - panic(fmt.Sprintf("Unknown error code %d in FfiConverterError.Read()", errorID)) + panic(fmt.Sprintf("invalid enum value %v in FfiConverterTimeBound.Read()", id)) } - } -func (c FfiConverterError) Write(writer io.Writer, value *Error) { - switch variantValue := value.err.(type) { - case *ErrorIrohBind: +func (FfiConverterTimeBound) Write(writer io.Writer, value TimeBound) { + switch variant_value := value.(type) { + case TimeBoundUnbounded: writeInt32(writer, 1) - case *ErrorInvalidUrl: + case TimeBoundIncluded: writeInt32(writer, 2) - case *ErrorIrohConnect: + FfiConverterUint64INSTANCE.Write(writer, variant_value.Field0) + case TimeBoundExcluded: writeInt32(writer, 3) - case *ErrorInvalidNetworkAddress: - writeInt32(writer, 4) - case *ErrorMissingConnection: - writeInt32(writer, 5) - case *ErrorInvalidPublicKey: - writeInt32(writer, 6) - case *ErrorIrpc: - writeInt32(writer, 7) + FfiConverterUint64INSTANCE.Write(writer, variant_value.Field0) default: - _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiConverterError.Write", value)) + _ = variant_value + panic(fmt.Sprintf("invalid enum value `%v` in FfiConverterTimeBound.Write", value)) } } -type FfiDestroyerError struct{} +type FfiDestroyerTimeBound struct{} -func (_ FfiDestroyerError) Destroy(value *Error) { - switch variantValue := value.err.(type) { - case ErrorIrohBind: - variantValue.destroy() - case ErrorInvalidUrl: - variantValue.destroy() - case ErrorIrohConnect: - variantValue.destroy() - case ErrorInvalidNetworkAddress: - variantValue.destroy() - case ErrorMissingConnection: - variantValue.destroy() - case ErrorInvalidPublicKey: - variantValue.destroy() - case ErrorIrpc: - variantValue.destroy() - default: - _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerError.Destroy", value)) - } +func (_ FfiDestroyerTimeBound) Destroy(value TimeBound) { + value.Destroy() } -type SpError struct { +// Error getting the next item from a subscription. +type WriteError struct { err error } -// Convience method to turn *SpError into error +// Convience method to turn *WriteError into error // Avoiding treating nil pointer as non nil error interface -func (err *SpError) AsError() error { +func (err *WriteError) AsError() error { if err == nil { return nil } else { @@ -2012,107 +4765,93 @@ func (err *SpError) AsError() error { } } -func (err SpError) Error() string { - return fmt.Sprintf("SpError: %s", err.err.Error()) +func (err WriteError) Error() string { + return fmt.Sprintf("WriteError: %s", err.err.Error()) } -func (err SpError) Unwrap() error { +func (err WriteError) Unwrap() error { return err.err } // Err* are used for checking error type with `errors.Is` -var ErrSpErrorNoCertificateChainFound = fmt.Errorf("SpErrorNoCertificateChainFound") -var ErrSpErrorC2paError = fmt.Errorf("SpErrorC2paError") +var ErrWriteErrorPrivateKeySize = fmt.Errorf("WriteErrorPrivateKeySize") // Variant structs -type SpErrorNoCertificateChainFound struct { - message string +// The provided private key is invalid (not 32 bytes). +type WriteErrorPrivateKeySize struct { + Size uint64 } -func NewSpErrorNoCertificateChainFound() *SpError { - return &SpError{err: &SpErrorNoCertificateChainFound{}} -} - -func (e SpErrorNoCertificateChainFound) destroy() { -} - -func (err SpErrorNoCertificateChainFound) Error() string { - return fmt.Sprintf("NoCertificateChainFound: %s", err.message) -} - -func (self SpErrorNoCertificateChainFound) Is(target error) bool { - return target == ErrSpErrorNoCertificateChainFound -} - -type SpErrorC2paError struct { - message string +// The provided private key is invalid (not 32 bytes). +func NewWriteErrorPrivateKeySize( + size uint64, +) *WriteError { + return &WriteError{err: &WriteErrorPrivateKeySize{ + Size: size}} } -func NewSpErrorC2paError() *SpError { - return &SpError{err: &SpErrorC2paError{}} +func (e WriteErrorPrivateKeySize) destroy() { + FfiDestroyerUint64{}.Destroy(e.Size) } -func (e SpErrorC2paError) destroy() { -} +func (err WriteErrorPrivateKeySize) Error() string { + return fmt.Sprint("PrivateKeySize", + ": ", -func (err SpErrorC2paError) Error() string { - return fmt.Sprintf("C2paError: %s", err.message) + "Size=", + err.Size, + ) } -func (self SpErrorC2paError) Is(target error) bool { - return target == ErrSpErrorC2paError +func (self WriteErrorPrivateKeySize) Is(target error) bool { + return target == ErrWriteErrorPrivateKeySize } -type FfiConverterSpError struct{} +type FfiConverterWriteError struct{} -var FfiConverterSpErrorINSTANCE = FfiConverterSpError{} +var FfiConverterWriteErrorINSTANCE = FfiConverterWriteError{} -func (c FfiConverterSpError) Lift(eb RustBufferI) *SpError { - return LiftFromRustBuffer[*SpError](c, eb) +func (c FfiConverterWriteError) Lift(eb RustBufferI) *WriteError { + return LiftFromRustBuffer[*WriteError](c, eb) } -func (c FfiConverterSpError) Lower(value *SpError) C.RustBuffer { - return LowerIntoRustBuffer[*SpError](c, value) +func (c FfiConverterWriteError) Lower(value *WriteError) C.RustBuffer { + return LowerIntoRustBuffer[*WriteError](c, value) } -func (c FfiConverterSpError) Read(reader io.Reader) *SpError { +func (c FfiConverterWriteError) Read(reader io.Reader) *WriteError { errorID := readUint32(reader) - message := FfiConverterStringINSTANCE.Read(reader) switch errorID { case 1: - return &SpError{&SpErrorNoCertificateChainFound{message}} - case 2: - return &SpError{&SpErrorC2paError{message}} + return &WriteError{&WriteErrorPrivateKeySize{ + Size: FfiConverterUint64INSTANCE.Read(reader), + }} default: - panic(fmt.Sprintf("Unknown error code %d in FfiConverterSpError.Read()", errorID)) + panic(fmt.Sprintf("Unknown error code %d in FfiConverterWriteError.Read()", errorID)) } - } -func (c FfiConverterSpError) Write(writer io.Writer, value *SpError) { +func (c FfiConverterWriteError) Write(writer io.Writer, value *WriteError) { switch variantValue := value.err.(type) { - case *SpErrorNoCertificateChainFound: + case *WriteErrorPrivateKeySize: writeInt32(writer, 1) - case *SpErrorC2paError: - writeInt32(writer, 2) + FfiConverterUint64INSTANCE.Write(writer, variantValue.Size) default: _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiConverterSpError.Write", value)) + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterWriteError.Write", value)) } } -type FfiDestroyerSpError struct{} +type FfiDestroyerWriteError struct{} -func (_ FfiDestroyerSpError) Destroy(value *SpError) { +func (_ FfiDestroyerWriteError) Destroy(value *WriteError) { switch variantValue := value.err.(type) { - case SpErrorNoCertificateChainFound: - variantValue.destroy() - case SpErrorC2paError: + case WriteErrorPrivateKeySize: variantValue.destroy() default: _ = variantValue - panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerSpError.Destroy", value)) + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerWriteError.Destroy", value)) } } @@ -2153,6 +4892,80 @@ func (_ FfiDestroyerOptionalString) Destroy(value *string) { } } +type FfiConverterOptionalBytes struct{} + +var FfiConverterOptionalBytesINSTANCE = FfiConverterOptionalBytes{} + +func (c FfiConverterOptionalBytes) Lift(rb RustBufferI) *[]byte { + return LiftFromRustBuffer[*[]byte](c, rb) +} + +func (_ FfiConverterOptionalBytes) Read(reader io.Reader) *[]byte { + if readInt8(reader) == 0 { + return nil + } + temp := FfiConverterBytesINSTANCE.Read(reader) + return &temp +} + +func (c FfiConverterOptionalBytes) Lower(value *[]byte) C.RustBuffer { + return LowerIntoRustBuffer[*[]byte](c, value) +} + +func (_ FfiConverterOptionalBytes) Write(writer io.Writer, value *[]byte) { + if value == nil { + writeInt8(writer, 0) + } else { + writeInt8(writer, 1) + FfiConverterBytesINSTANCE.Write(writer, *value) + } +} + +type FfiDestroyerOptionalBytes struct{} + +func (_ FfiDestroyerOptionalBytes) Destroy(value *[]byte) { + if value != nil { + FfiDestroyerBytes{}.Destroy(*value) + } +} + +type FfiConverterOptionalSubscribeItem struct{} + +var FfiConverterOptionalSubscribeItemINSTANCE = FfiConverterOptionalSubscribeItem{} + +func (c FfiConverterOptionalSubscribeItem) Lift(rb RustBufferI) *SubscribeItem { + return LiftFromRustBuffer[*SubscribeItem](c, rb) +} + +func (_ FfiConverterOptionalSubscribeItem) Read(reader io.Reader) *SubscribeItem { + if readInt8(reader) == 0 { + return nil + } + temp := FfiConverterSubscribeItemINSTANCE.Read(reader) + return &temp +} + +func (c FfiConverterOptionalSubscribeItem) Lower(value *SubscribeItem) C.RustBuffer { + return LowerIntoRustBuffer[*SubscribeItem](c, value) +} + +func (_ FfiConverterOptionalSubscribeItem) Write(writer io.Writer, value *SubscribeItem) { + if value == nil { + writeInt8(writer, 0) + } else { + writeInt8(writer, 1) + FfiConverterSubscribeItemINSTANCE.Write(writer, *value) + } +} + +type FfiDestroyerOptionalSubscribeItem struct{} + +func (_ FfiDestroyerOptionalSubscribeItem) Destroy(value *SubscribeItem) { + if value != nil { + FfiDestroyerSubscribeItem{}.Destroy(*value) + } +} + type FfiConverterSequenceString struct{} var FfiConverterSequenceStringINSTANCE = FfiConverterSequenceString{} @@ -2196,6 +5009,92 @@ func (FfiDestroyerSequenceString) Destroy(sequence []string) { } } +type FfiConverterSequencePublicKey struct{} + +var FfiConverterSequencePublicKeyINSTANCE = FfiConverterSequencePublicKey{} + +func (c FfiConverterSequencePublicKey) Lift(rb RustBufferI) []*PublicKey { + return LiftFromRustBuffer[[]*PublicKey](c, rb) +} + +func (c FfiConverterSequencePublicKey) Read(reader io.Reader) []*PublicKey { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]*PublicKey, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterPublicKeyINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequencePublicKey) Lower(value []*PublicKey) C.RustBuffer { + return LowerIntoRustBuffer[[]*PublicKey](c, value) +} + +func (c FfiConverterSequencePublicKey) Write(writer io.Writer, value []*PublicKey) { + if len(value) > math.MaxInt32 { + panic("[]*PublicKey is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterPublicKeyINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequencePublicKey struct{} + +func (FfiDestroyerSequencePublicKey) Destroy(sequence []*PublicKey) { + for _, value := range sequence { + FfiDestroyerPublicKey{}.Destroy(value) + } +} + +type FfiConverterSequenceEntry struct{} + +var FfiConverterSequenceEntryINSTANCE = FfiConverterSequenceEntry{} + +func (c FfiConverterSequenceEntry) Lift(rb RustBufferI) []Entry { + return LiftFromRustBuffer[[]Entry](c, rb) +} + +func (c FfiConverterSequenceEntry) Read(reader io.Reader) []Entry { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]Entry, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterEntryINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceEntry) Lower(value []Entry) C.RustBuffer { + return LowerIntoRustBuffer[[]Entry](c, value) +} + +func (c FfiConverterSequenceEntry) Write(writer io.Writer, value []Entry) { + if len(value) > math.MaxInt32 { + panic("[]Entry is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterEntryINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceEntry struct{} + +func (FfiDestroyerSequenceEntry) Destroy(sequence []Entry) { + for _, value := range sequence { + FfiDestroyerEntry{}.Destroy(value) + } +} + const ( uniffiRustFuturePollReady int8 = 0 uniffiRustFuturePollMaybeReady int8 = 1 @@ -2259,7 +5158,7 @@ func iroh_streamplace_uniffiFreeGorutine(data C.uint64_t) { guard <- struct{}{} } -func GetManifestAndCert(data []byte) (string, *SpError) { +func GetManifestAndCert(data []byte) (string, error) { _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { return GoRustBuffer{ inner: C.uniffi_iroh_streamplace_fn_func_get_manifest_and_cert(FfiConverterBytesINSTANCE.Lower(data), _uniffiStatus), @@ -2269,11 +5168,11 @@ func GetManifestAndCert(data []byte) (string, *SpError) { var _uniffiDefaultValue string return _uniffiDefaultValue, _uniffiErr } else { - return FfiConverterStringINSTANCE.Lift(_uniffiRV), _uniffiErr + return FfiConverterStringINSTANCE.Lift(_uniffiRV), nil } } -func Sign(manifest string, data []byte, certs []byte, gosigner GoSigner) ([]byte, *SpError) { +func Sign(manifest string, data []byte, certs []byte, gosigner GoSigner) ([]byte, error) { _uniffiRV, _uniffiErr := rustCallWithError[SpError](FfiConverterSpError{}, func(_uniffiStatus *C.RustCallStatus) RustBufferI { return GoRustBuffer{ inner: C.uniffi_iroh_streamplace_fn_func_sign(FfiConverterStringINSTANCE.Lower(manifest), FfiConverterBytesINSTANCE.Lower(data), FfiConverterBytesINSTANCE.Lower(certs), FfiConverterGoSignerINSTANCE.Lower(gosigner), _uniffiStatus), @@ -2283,6 +5182,14 @@ func Sign(manifest string, data []byte, certs []byte, gosigner GoSigner) ([]byte var _uniffiDefaultValue []byte return _uniffiDefaultValue, _uniffiErr } else { - return FfiConverterBytesINSTANCE.Lift(_uniffiRV), _uniffiErr + return FfiConverterBytesINSTANCE.Lift(_uniffiRV), nil } } + +func SubscribeItemDebug(item SubscribeItem) string { + return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return GoRustBuffer{ + inner: C.uniffi_iroh_streamplace_fn_func_subscribe_item_debug(FfiConverterSubscribeItemINSTANCE.Lower(item), _uniffiStatus), + } + })) +} diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 7f9029d6..b4fecb00 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -391,6 +391,20 @@ static void call_UniffiCallbackInterfaceDataHandlerMethod0( } +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_DATA_HANDLER_OLD_METHOD0 +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_DATA_HANDLER_OLD_METHOD0 +typedef void (*UniffiCallbackInterfaceDataHandlerOldMethod0)(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); + +// Making function static works arround: +// https://github.com/golang/go/issues/11263 +static void call_UniffiCallbackInterfaceDataHandlerOldMethod0( + UniffiCallbackInterfaceDataHandlerOldMethod0 cb, uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return) +{ + return cb(uniffi_handle, topic, data, uniffi_future_callback, uniffi_callback_data, uniffi_out_return); +} + + #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 @@ -413,6 +427,14 @@ typedef struct UniffiVTableCallbackInterfaceDataHandler { UniffiCallbackInterfaceFree uniffiFree; } UniffiVTableCallbackInterfaceDataHandler; +#endif +#ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER_OLD +#define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER_OLD +typedef struct UniffiVTableCallbackInterfaceDataHandlerOld { + UniffiCallbackInterfaceDataHandlerOldMethod0 handleData; + UniffiCallbackInterfaceFree uniffiFree; +} UniffiVTableCallbackInterfaceDataHandlerOld; + #endif #ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER #define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER @@ -442,6 +464,56 @@ void uniffi_iroh_streamplace_fn_init_callback_vtable_datahandler(UniffiVTableCal uint64_t uniffi_iroh_streamplace_fn_method_datahandler_handle_data(void* ptr, RustBuffer topic, RustBuffer data ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLEROLD +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLEROLD +void* uniffi_iroh_streamplace_fn_clone_datahandlerold(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DATAHANDLEROLD +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DATAHANDLEROLD +void uniffi_iroh_streamplace_fn_free_datahandlerold(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_DATAHANDLEROLD +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_DATAHANDLEROLD +void uniffi_iroh_streamplace_fn_init_callback_vtable_datahandlerold(UniffiVTableCallbackInterfaceDataHandlerOld* vtable +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DATAHANDLEROLD_HANDLE_DATA +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DATAHANDLEROLD_HANDLE_DATA +uint64_t uniffi_iroh_streamplace_fn_method_datahandlerold_handle_data(void* ptr, RustBuffer topic, RustBuffer data +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DB +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DB +void* uniffi_iroh_streamplace_fn_clone_db(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DB +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DB +void uniffi_iroh_streamplace_fn_free_db(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_ITER_WITH_OPTS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_ITER_WITH_OPTS +uint64_t uniffi_iroh_streamplace_fn_method_db_iter_with_opts(void* ptr, void* filter +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_SUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_SUBSCRIBE +void* uniffi_iroh_streamplace_fn_method_db_subscribe(void* ptr, void* filter, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_SUBSCRIBE_WITH_OPTS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_SUBSCRIBE_WITH_OPTS +void* uniffi_iroh_streamplace_fn_method_db_subscribe_with_opts(void* ptr, RustBuffer opts, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_WRITE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DB_WRITE +void* uniffi_iroh_streamplace_fn_method_db_write(void* ptr, RustBuffer secret, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_ENDPOINT #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_ENDPOINT void* uniffi_iroh_streamplace_fn_clone_endpoint(void* ptr, RustCallStatus *out_status @@ -463,6 +535,57 @@ uint64_t uniffi_iroh_streamplace_fn_constructor_endpoint_new(void uint64_t uniffi_iroh_streamplace_fn_method_endpoint_node_addr(void* ptr ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_FILTER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_FILTER +void* uniffi_iroh_streamplace_fn_clone_filter(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_FILTER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_FILTER +void uniffi_iroh_streamplace_fn_free_filter(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_FILTER_NEW +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_FILTER_NEW +void* uniffi_iroh_streamplace_fn_constructor_filter_new(RustCallStatus *out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_GLOBAL +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_GLOBAL +void* uniffi_iroh_streamplace_fn_method_filter_global(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_SCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_SCOPE +void* uniffi_iroh_streamplace_fn_method_filter_scope(void* ptr, void* scope, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_SCOPES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_SCOPES +void* uniffi_iroh_streamplace_fn_method_filter_scopes(void* ptr, RustBuffer scopes, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_STREAM +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_STREAM +void* uniffi_iroh_streamplace_fn_method_filter_stream(void* ptr, RustBuffer stream, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIME_FROM +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIME_FROM +void* uniffi_iroh_streamplace_fn_method_filter_time_from(void* ptr, uint64_t min, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIME_RANGE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIME_RANGE +void* uniffi_iroh_streamplace_fn_method_filter_time_range(void* ptr, uint64_t min, uint64_t max, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIMESTAMPS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_FILTER_TIMESTAMPS +void* uniffi_iroh_streamplace_fn_method_filter_timestamps(void* ptr, RustBuffer min, RustBuffer max, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_GOSIGNER #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_GOSIGNER void* uniffi_iroh_streamplace_fn_clone_gosigner(void* ptr, RustCallStatus *out_status @@ -483,6 +606,71 @@ void uniffi_iroh_streamplace_fn_init_callback_vtable_gosigner(UniffiVTableCallba RustBuffer uniffi_iroh_streamplace_fn_method_gosigner_sign(void* ptr, RustBuffer data, RustCallStatus *out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODE +void* uniffi_iroh_streamplace_fn_clone_node(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_NODE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_NODE +void uniffi_iroh_streamplace_fn_free_node(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_FORWARDER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_FORWARDER +uint64_t uniffi_iroh_streamplace_fn_constructor_node_forwarder(RustBuffer config +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_RECEIVER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_RECEIVER +uint64_t uniffi_iroh_streamplace_fn_constructor_node_receiver(RustBuffer config, void* handler +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_SENDER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_NODE_SENDER +uint64_t uniffi_iroh_streamplace_fn_constructor_node_sender(RustBuffer config +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_DB +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_DB +void* uniffi_iroh_streamplace_fn_method_node_db(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_JOIN_PEERS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_JOIN_PEERS +uint64_t uniffi_iroh_streamplace_fn_method_node_join_peers(void* ptr, RustBuffer peers +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_NODE_ID +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_NODE_ID +uint64_t uniffi_iroh_streamplace_fn_method_node_node_id(void* ptr +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_NODE_SCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_NODE_SCOPE +void* uniffi_iroh_streamplace_fn_method_node_node_scope(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_SEND_SEGMENT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_SEND_SEGMENT +uint64_t uniffi_iroh_streamplace_fn_method_node_send_segment(void* ptr, RustBuffer key, RustBuffer data +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_SUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_SUBSCRIBE +uint64_t uniffi_iroh_streamplace_fn_method_node_subscribe(void* ptr, RustBuffer key, void* remote_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_TICKET +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_TICKET +uint64_t uniffi_iroh_streamplace_fn_method_node_ticket(void* ptr +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_UNSUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_UNSUBSCRIBE +uint64_t uniffi_iroh_streamplace_fn_method_node_unsubscribe(void* ptr, RustBuffer key, void* remote_id +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODEADDR #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_NODEADDR void* uniffi_iroh_streamplace_fn_clone_nodeaddr(void* ptr, RustCallStatus *out_status @@ -538,6 +726,11 @@ void* uniffi_iroh_streamplace_fn_constructor_publickey_from_bytes(RustBuffer byt void* uniffi_iroh_streamplace_fn_constructor_publickey_from_string(RustBuffer s, RustCallStatus *out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_AS_VEC +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_AS_VEC +RustBuffer uniffi_iroh_streamplace_fn_method_publickey_as_vec(void* ptr, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_EQUAL #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_EQUAL int8_t uniffi_iroh_streamplace_fn_method_publickey_equal(void* ptr, void* other, RustCallStatus *out_status @@ -548,16 +741,51 @@ int8_t uniffi_iroh_streamplace_fn_method_publickey_equal(void* ptr, void* other, RustBuffer uniffi_iroh_streamplace_fn_method_publickey_fmt_short(void* ptr, RustCallStatus *out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_TO_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_TO_BYTES -RustBuffer uniffi_iroh_streamplace_fn_method_publickey_to_bytes(void* ptr, RustCallStatus *out_status -); -#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_UNIFFI_TRAIT_DISPLAY #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEY_UNIFFI_TRAIT_DISPLAY RustBuffer uniffi_iroh_streamplace_fn_method_publickey_uniffi_trait_display(void* ptr, RustCallStatus *out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_PUBLICKEYOLD +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_PUBLICKEYOLD +void* uniffi_iroh_streamplace_fn_clone_publickeyold(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_PUBLICKEYOLD +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_PUBLICKEYOLD +void uniffi_iroh_streamplace_fn_free_publickeyold(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES +void* uniffi_iroh_streamplace_fn_constructor_publickeyold_from_bytes(RustBuffer bytes, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING +void* uniffi_iroh_streamplace_fn_constructor_publickeyold_from_string(RustBuffer s, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_EQUAL +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_EQUAL +int8_t uniffi_iroh_streamplace_fn_method_publickeyold_equal(void* ptr, void* other, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_FMT_SHORT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_FMT_SHORT +RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_fmt_short(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_TO_BYTES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_TO_BYTES +RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_to_bytes(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_UNIFFI_TRAIT_DISPLAY +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_UNIFFI_TRAIT_DISPLAY +RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_uniffi_trait_display(void* ptr, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER void* uniffi_iroh_streamplace_fn_clone_receiver(void* ptr, RustCallStatus *out_status @@ -613,6 +841,41 @@ uint64_t uniffi_iroh_streamplace_fn_method_sender_node_addr(void* ptr uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key, RustBuffer data ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SUBSCRIBERESPONSE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SUBSCRIBERESPONSE +void* uniffi_iroh_streamplace_fn_clone_subscriberesponse(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_SUBSCRIBERESPONSE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_SUBSCRIBERESPONSE +void uniffi_iroh_streamplace_fn_free_subscriberesponse(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SUBSCRIBERESPONSE_NEXT_RAW +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SUBSCRIBERESPONSE_NEXT_RAW +uint64_t uniffi_iroh_streamplace_fn_method_subscriberesponse_next_raw(void* ptr +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SUBSCRIBERESPONSE_UNIFFI_TRAIT_DEBUG +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SUBSCRIBERESPONSE_UNIFFI_TRAIT_DEBUG +RustBuffer uniffi_iroh_streamplace_fn_method_subscriberesponse_uniffi_trait_debug(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_WRITESCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_WRITESCOPE +void* uniffi_iroh_streamplace_fn_clone_writescope(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_WRITESCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_WRITESCOPE +void uniffi_iroh_streamplace_fn_free_writescope(void* ptr, RustCallStatus *out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_WRITESCOPE_PUT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_WRITESCOPE_PUT +uint64_t uniffi_iroh_streamplace_fn_method_writescope_put(void* ptr, RustBuffer stream, RustBuffer key, RustBuffer value +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST_AND_CERT #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_GET_MANIFEST_AND_CERT RustBuffer uniffi_iroh_streamplace_fn_func_get_manifest_and_cert(RustBuffer data, RustCallStatus *out_status @@ -623,6 +886,11 @@ RustBuffer uniffi_iroh_streamplace_fn_func_get_manifest_and_cert(RustBuffer data RustBuffer uniffi_iroh_streamplace_fn_func_sign(RustBuffer manifest, RustBuffer data, RustBuffer certs, void* gosigner, RustCallStatus *out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_SUBSCRIBE_ITEM_DEBUG +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FUNC_SUBSCRIBE_ITEM_DEBUG +RustBuffer uniffi_iroh_streamplace_fn_func_subscribe_item_debug(RustBuffer item, RustCallStatus *out_status +); +#endif #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC #define UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_RUSTBUFFER_ALLOC RustBuffer ffi_iroh_streamplace_rustbuffer_alloc(uint64_t size, RustCallStatus *out_status @@ -913,24 +1181,150 @@ uint16_t uniffi_iroh_streamplace_checksum_func_get_manifest_and_cert(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_SIGN uint16_t uniffi_iroh_streamplace_checksum_func_sign(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_SUBSCRIBE_ITEM_DEBUG +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_FUNC_SUBSCRIBE_ITEM_DEBUG +uint16_t uniffi_iroh_streamplace_checksum_func_subscribe_item_debug(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLER_HANDLE_DATA #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLER_HANDLE_DATA uint16_t uniffi_iroh_streamplace_checksum_method_datahandler_handle_data(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLEROLD_HANDLE_DATA +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLEROLD_HANDLE_DATA +uint16_t uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_ITER_WITH_OPTS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_ITER_WITH_OPTS +uint16_t uniffi_iroh_streamplace_checksum_method_db_iter_with_opts(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_SUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_SUBSCRIBE +uint16_t uniffi_iroh_streamplace_checksum_method_db_subscribe(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_SUBSCRIBE_WITH_OPTS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_SUBSCRIBE_WITH_OPTS +uint16_t uniffi_iroh_streamplace_checksum_method_db_subscribe_with_opts(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_WRITE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_WRITE +uint16_t uniffi_iroh_streamplace_checksum_method_db_write(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_ENDPOINT_NODE_ADDR #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_ENDPOINT_NODE_ADDR uint16_t uniffi_iroh_streamplace_checksum_method_endpoint_node_addr(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_GLOBAL +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_GLOBAL +uint16_t uniffi_iroh_streamplace_checksum_method_filter_global(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_SCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_SCOPE +uint16_t uniffi_iroh_streamplace_checksum_method_filter_scope(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_SCOPES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_SCOPES +uint16_t uniffi_iroh_streamplace_checksum_method_filter_scopes(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_STREAM +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_STREAM +uint16_t uniffi_iroh_streamplace_checksum_method_filter_stream(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIME_FROM +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIME_FROM +uint16_t uniffi_iroh_streamplace_checksum_method_filter_time_from(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIME_RANGE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIME_RANGE +uint16_t uniffi_iroh_streamplace_checksum_method_filter_time_range(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIMESTAMPS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_TIMESTAMPS +uint16_t uniffi_iroh_streamplace_checksum_method_filter_timestamps(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_GOSIGNER_SIGN #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_GOSIGNER_SIGN uint16_t uniffi_iroh_streamplace_checksum_method_gosigner_sign(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_DB +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_DB +uint16_t uniffi_iroh_streamplace_checksum_method_node_db(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_JOIN_PEERS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_JOIN_PEERS +uint16_t uniffi_iroh_streamplace_checksum_method_node_join_peers(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_NODE_ID +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_NODE_ID +uint16_t uniffi_iroh_streamplace_checksum_method_node_node_id(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_NODE_SCOPE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_NODE_SCOPE +uint16_t uniffi_iroh_streamplace_checksum_method_node_node_scope(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_SEND_SEGMENT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_SEND_SEGMENT +uint16_t uniffi_iroh_streamplace_checksum_method_node_send_segment(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_SUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_SUBSCRIBE +uint16_t uniffi_iroh_streamplace_checksum_method_node_subscribe(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_TICKET +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_TICKET +uint16_t uniffi_iroh_streamplace_checksum_method_node_ticket(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_UNSUBSCRIBE +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_UNSUBSCRIBE +uint16_t uniffi_iroh_streamplace_checksum_method_node_unsubscribe(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODEADDR_DIRECT_ADDRESSES @@ -955,6 +1349,12 @@ uint16_t uniffi_iroh_streamplace_checksum_method_nodeaddr_node_id(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODEADDR_RELAY_URL uint16_t uniffi_iroh_streamplace_checksum_method_nodeaddr_relay_url(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_AS_VEC +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_AS_VEC +uint16_t uniffi_iroh_streamplace_checksum_method_publickey_as_vec(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_EQUAL @@ -969,9 +1369,21 @@ uint16_t uniffi_iroh_streamplace_checksum_method_publickey_fmt_short(void ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_TO_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_TO_BYTES -uint16_t uniffi_iroh_streamplace_checksum_method_publickey_to_bytes(void +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_EQUAL +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_EQUAL +uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_equal(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_FMT_SHORT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_FMT_SHORT +uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_TO_BYTES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_TO_BYTES +uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes(void ); #endif @@ -1003,12 +1415,48 @@ uint16_t uniffi_iroh_streamplace_checksum_method_sender_node_addr(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SENDER_SEND uint16_t uniffi_iroh_streamplace_checksum_method_sender_send(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SUBSCRIBERESPONSE_NEXT_RAW +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SUBSCRIBERESPONSE_NEXT_RAW +uint16_t uniffi_iroh_streamplace_checksum_method_subscriberesponse_next_raw(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_WRITESCOPE_PUT +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_WRITESCOPE_PUT +uint16_t uniffi_iroh_streamplace_checksum_method_writescope_put(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_ENDPOINT_NEW #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_ENDPOINT_NEW uint16_t uniffi_iroh_streamplace_checksum_constructor_endpoint_new(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_FILTER_NEW +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_FILTER_NEW +uint16_t uniffi_iroh_streamplace_checksum_constructor_filter_new(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_FORWARDER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_FORWARDER +uint16_t uniffi_iroh_streamplace_checksum_constructor_node_forwarder(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_RECEIVER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_RECEIVER +uint16_t uniffi_iroh_streamplace_checksum_constructor_node_receiver(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_SENDER +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODE_SENDER +uint16_t uniffi_iroh_streamplace_checksum_constructor_node_sender(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_NODEADDR_NEW @@ -1027,6 +1475,18 @@ uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_bytes(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEY_FROM_STRING uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_string(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES +uint16_t uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING +uint16_t uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_RECEIVER_NEW @@ -1050,6 +1510,8 @@ uint32_t ffi_iroh_streamplace_uniffi_contract_version(void void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerMethod0(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerFree(uint64_t handle); + void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); + void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree(uint64_t handle); void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uint64_t uniffi_handle, RustBuffer data, RustBuffer* uniffi_out_return, RustCallStatus* callStatus ); void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(uint64_t handle); diff --git a/rust/iroh-streamplace/src/endpoint.rs b/rust/iroh-streamplace/src/endpoint.rs index f81c5bf8..b3662d2e 100644 --- a/rust/iroh-streamplace/src/endpoint.rs +++ b/rust/iroh-streamplace/src/endpoint.rs @@ -1,6 +1,6 @@ use iroh::Watcher; -use crate::{error::Error, utils::NodeAddr}; +use crate::{error::Error, node_addr::NodeAddr}; #[derive(uniffi::Object, Debug, Clone)] pub struct Endpoint { diff --git a/rust/iroh-streamplace/src/error.rs b/rust/iroh-streamplace/src/error.rs index 2b260311..1314ca55 100644 --- a/rust/iroh-streamplace/src/error.rs +++ b/rust/iroh-streamplace/src/error.rs @@ -5,18 +5,11 @@ pub enum Error { #[snafu(display("Bind failure"), context(false))] IrohBind { source: iroh::endpoint::BindError }, - #[snafu(display("Invalid URL"), context(false))] - InvalidUrl { source: url::ParseError }, #[snafu(display("Failed to connect"), context(false))] IrohConnect { source: iroh::endpoint::ConnectError, }, - #[snafu(display("Invalid network address"), context(false))] - InvalidNetworkAddress { source: std::net::AddrParseError }, - #[snafu(display("No connection available"))] MissingConnection, - #[snafu(display("Invalid public key"))] - InvalidPublicKey, #[snafu(display("RPC error"), context(false))] Irpc { source: irpc::Error }, } diff --git a/rust/iroh-streamplace/src/key.rs b/rust/iroh-streamplace/src/key.rs deleted file mode 100644 index 423b53a2..00000000 --- a/rust/iroh-streamplace/src/key.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::str::FromStr; - -use crate::error::{Error, InvalidPublicKeySnafu}; - -/// A public key. -/// -/// The key itself is just a 32 byte array, but a key has associated crypto -/// information that is cached for performance reasons. -#[derive(Debug, Clone, Eq, uniffi::Object)] -#[uniffi::export(Display)] -pub struct PublicKeyOld { - pub(crate) key: [u8; 32], -} - -impl From for PublicKeyOld { - fn from(key: iroh::PublicKey) -> Self { - PublicKeyOld { - key: *key.as_bytes(), - } - } -} -impl From<&PublicKeyOld> for iroh::PublicKey { - fn from(key: &PublicKeyOld) -> Self { - iroh::PublicKey::from_bytes(&key.key).unwrap() - } -} - -#[uniffi::export] -impl PublicKeyOld { - /// Returns true if the PublicKeys are equal - pub fn equal(&self, other: &PublicKeyOld) -> bool { - *self == *other - } - - /// Express the PublicKey as a byte array - pub fn to_bytes(&self) -> Vec { - self.key.to_vec() - } - - /// Make a PublicKey from base32 string - #[uniffi::constructor] - pub fn from_string(s: String) -> Result { - let key = iroh::PublicKey::from_str(&s).map_err(|_| InvalidPublicKeySnafu.build())?; - Ok(key.into()) - } - - /// Make a PublicKey from byte array - #[uniffi::constructor] - pub fn from_bytes(bytes: Vec) -> Result { - if bytes.len() != 32 { - InvalidPublicKeySnafu.fail()?; - } - let bytes: [u8; 32] = bytes.try_into().expect("checked above"); - let key = iroh::PublicKey::from_bytes(&bytes).map_err(|_| InvalidPublicKeySnafu.build())?; - Ok(key.into()) - } - - /// Convert to a base32 string limited to the first 10 bytes for a friendly string - /// representation of the key. - pub fn fmt_short(&self) -> String { - iroh::PublicKey::from(self).fmt_short() - } -} - -impl PartialEq for PublicKeyOld { - fn eq(&self, other: &PublicKeyOld) -> bool { - self.key == other.key - } -} - -impl std::fmt::Display for PublicKeyOld { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - iroh::PublicKey::from(self).fmt(f) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_public_key() { - let key_str = - String::from("523c7996bad77424e96786cf7a7205115337a5b4565cd25506a0f297b191a5ea"); - let fmt_str = String::from("523c7996ba"); - let bytes = b"\x52\x3c\x79\x96\xba\xd7\x74\x24\xe9\x67\x86\xcf\x7a\x72\x05\x11\x53\x37\xa5\xb4\x56\x5c\xd2\x55\x06\xa0\xf2\x97\xb1\x91\xa5\xea"; - // - // create key from string - let key = PublicKeyOld::from_string(key_str.clone()).unwrap(); - // - // test methods are as expected - assert_eq!(key_str, key.to_string()); - assert_eq!(bytes.to_vec(), key.to_bytes()); - assert_eq!(fmt_str, key.fmt_short()); - // - // create key from bytes - let key_0 = PublicKeyOld::from_bytes(bytes.to_vec()).unwrap(); - // - // test methods are as expected - assert_eq!(key_str, key_0.to_string()); - assert_eq!(bytes.to_vec(), key_0.to_bytes()); - assert_eq!(fmt_str, key_0.fmt_short()); - // - // test that the eq function works - assert!(key.equal(&key_0)); - assert!(key_0.equal(&key)); - } -} diff --git a/rust/iroh-streamplace/src/lib.rs b/rust/iroh-streamplace/src/lib.rs index 5a1dcac5..52558b38 100644 --- a/rust/iroh-streamplace/src/lib.rs +++ b/rust/iroh-streamplace/src/lib.rs @@ -3,10 +3,10 @@ uniffi::setup_scaffolding!(); pub mod c2pa; pub mod endpoint; pub mod error; -pub mod key; +pub mod public_key; pub mod node; pub mod receiver; pub mod sender; -pub mod utils; +pub mod node_addr; mod api; diff --git a/rust/iroh-streamplace/src/node/db.rs b/rust/iroh-streamplace/src/node/db.rs index e0c42bfe..9323fc96 100644 --- a/rust/iroh-streamplace/src/node/db.rs +++ b/rust/iroh-streamplace/src/node/db.rs @@ -12,19 +12,19 @@ use ref_cast::RefCast; use snafu::Snafu; use tokio::sync::Mutex; +use crate::public_key::PublicKey; + use super::db; // the files here are just copied from iroh-smol-kv-uniffi/src/code mod kv { - mod public_key; - pub use public_key::PublicKey; mod time_bound; pub use time_bound::TimeBound; mod subscribe_mode; pub use subscribe_mode::SubscribeMode; } use db::util::format_bytes; -pub use kv::{PublicKey, SubscribeMode, TimeBound}; +pub use kv::{SubscribeMode, TimeBound}; /// Error creating a new database node. #[derive(Debug, Snafu, uniffi::Error)] diff --git a/rust/iroh-streamplace/src/node/streams.rs b/rust/iroh-streamplace/src/node/streams.rs index b5e3bff2..0e3b8d8b 100644 --- a/rust/iroh-streamplace/src/node/streams.rs +++ b/rust/iroh-streamplace/src/node/streams.rs @@ -644,7 +644,7 @@ impl Node { pub async fn subscribe( &self, key: String, - remote_id: Arc, + remote_id: Arc, ) -> Result<(), PutError> { self.api .rpc(api::Subscribe { @@ -661,7 +661,7 @@ impl Node { pub async fn unsubscribe( &self, key: String, - remote_id: Arc, + remote_id: Arc, ) -> Result<(), PutError> { self.api .rpc(api::Unsubscribe { @@ -721,7 +721,7 @@ impl Node { } /// Get this node's node ID. - pub async fn node_id(&self) -> Result, PutError> { + pub async fn node_id(&self) -> Result, PutError> { let addr = self .api .rpc(api::GetNodeAddr) diff --git a/rust/iroh-streamplace/src/node/tests.rs b/rust/iroh-streamplace/src/node/tests.rs index a08e7202..ec5d1897 100644 --- a/rust/iroh-streamplace/src/node/tests.rs +++ b/rust/iroh-streamplace/src/node/tests.rs @@ -8,7 +8,7 @@ use super::{streams::HandlerMode, *}; struct TestNode { node: Arc, - public: Arc, + public: Arc, ticket: String, #[allow(dead_code)] private: Vec, diff --git a/rust/iroh-streamplace/src/utils.rs b/rust/iroh-streamplace/src/node_addr.rs similarity index 72% rename from rust/iroh-streamplace/src/utils.rs rename to rust/iroh-streamplace/src/node_addr.rs index cc02d198..6796b351 100644 --- a/rust/iroh-streamplace/src/utils.rs +++ b/rust/iroh-streamplace/src/node_addr.rs @@ -1,11 +1,11 @@ use std::{str::FromStr, sync::Arc}; -use crate::{error::Error, key::PublicKeyOld}; +use crate::public_key::PublicKey; /// A peer and it's addressing information. #[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] pub struct NodeAddr { - node_id: Arc, + node_id: Arc, relay_url: Option, addresses: Vec, } @@ -14,7 +14,7 @@ pub struct NodeAddr { impl NodeAddr { /// Create a new [`NodeAddr`] with empty [`AddrInfo`]. #[uniffi::constructor] - pub fn new(node_id: &PublicKeyOld, derp_url: Option, addresses: Vec) -> Self { + pub fn new(node_id: &PublicKey, derp_url: Option, addresses: Vec) -> Self { Self { node_id: Arc::new(node_id.clone()), relay_url: derp_url, @@ -22,7 +22,7 @@ impl NodeAddr { } } - pub fn node_id(&self) -> PublicKeyOld { + pub fn node_id(&self) -> PublicKey { self.node_id.as_ref().clone() } @@ -42,15 +42,27 @@ impl NodeAddr { } } +/// Error when converting from ffi NodeAddr to iroh::NodeAddr +#[derive(Debug, snafu::Snafu, uniffi::Error)] +#[uniffi(flat_error)] +#[snafu(visibility(pub(crate)))] +pub enum NodeAddrError { + #[snafu(display("Invalid URL"), context(false))] + InvalidUrl { source: url::ParseError }, + #[snafu(display("Invalid network address"), context(false))] + InvalidNetworkAddress { source: std::net::AddrParseError }, +} + + impl TryFrom for iroh::NodeAddr { - type Error = Error; + type Error = NodeAddrError; fn try_from(value: NodeAddr) -> Result { let mut node_addr = iroh::NodeAddr::new((&*value.node_id).into()); let addresses = value .direct_addresses() .into_iter() - .map(|addr| std::net::SocketAddr::from_str(&addr).map_err(Error::from)) + .map(|addr| std::net::SocketAddr::from_str(&addr)) .collect::, _>>()?; if let Some(derp_url) = value.relay_url() { diff --git a/rust/iroh-streamplace/src/node/db/kv/public_key.rs b/rust/iroh-streamplace/src/public_key.rs similarity index 100% rename from rust/iroh-streamplace/src/node/db/kv/public_key.rs rename to rust/iroh-streamplace/src/public_key.rs diff --git a/rust/iroh-streamplace/src/receiver.rs b/rust/iroh-streamplace/src/receiver.rs index 9a995a57..23d2df7a 100644 --- a/rust/iroh-streamplace/src/receiver.rs +++ b/rust/iroh-streamplace/src/receiver.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use iroh::protocol::Router; -use crate::{api::Api, endpoint::Endpoint, error::Error, key::PublicKeyOld, utils::NodeAddr}; +use crate::{api::Api, endpoint::Endpoint, error::Error, public_key::PublicKey, node_addr::NodeAddr}; #[derive(uniffi::Object)] pub struct Receiver { @@ -38,7 +38,7 @@ impl Receiver { /// Subscribe to the given topic on the remote. #[uniffi::method(async_runtime = "tokio")] - pub async fn subscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { + pub async fn subscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { let remote_id: iroh::NodeId = remote_id.as_ref().into(); let api = Api::connect(self.endpoint.endpoint.clone(), remote_id); api.subscribe(topic.to_string(), self.endpoint.endpoint.node_id()) @@ -50,7 +50,7 @@ impl Receiver { #[uniffi::method(async_runtime = "tokio")] pub async fn unsubscribe( &self, - remote_id: Arc, + remote_id: Arc, topic: &str, ) -> Result<(), Error> { let remote_id: iroh::NodeId = remote_id.as_ref().into(); diff --git a/rust/iroh-streamplace/src/sender.rs b/rust/iroh-streamplace/src/sender.rs index 99ec5ae6..15cfa34d 100644 --- a/rust/iroh-streamplace/src/sender.rs +++ b/rust/iroh-streamplace/src/sender.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use iroh::protocol::Router; -use crate::{api::Api, endpoint::Endpoint, error::Error, utils::NodeAddr}; +use crate::{api::Api, endpoint::Endpoint, error::Error, node_addr::NodeAddr}; #[derive(uniffi::Object)] pub struct Sender { -- 2.51.2 From d8d93deb7688500bd75ac9d1b7b33233d8a409fb Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 14:32:49 -0700 Subject: [PATCH 077/525] chat: fix & unify detection of canModerate --- .../components/live-dashboard/bento-grid.tsx | 2 - js/app/components/mobile/chat.tsx | 3 +- js/app/src/screens/chat-popout.native.tsx | 2 +- js/app/src/screens/chat-popout.tsx | 3 +- js/components/src/components/chat/chat.tsx | 194 ++++++++---------- .../src/components/chat/mod-view.tsx | 5 +- .../src/components/dashboard/chat-panel.tsx | 4 +- .../src/player-store/player-store.tsx | 10 + 8 files changed, 108 insertions(+), 115 deletions(-) diff --git a/js/app/components/live-dashboard/bento-grid.tsx b/js/app/components/live-dashboard/bento-grid.tsx index 89db5b89..a9d456e9 100644 --- a/js/app/components/live-dashboard/bento-grid.tsx +++ b/js/app/components/live-dashboard/bento-grid.tsx @@ -69,7 +69,6 @@ export default function BentoGrid({ // Calculate derived values const isConnected = ingestConnectionState === "connected"; - const canModerate = isLive && isConnected; // Calculate uptime const getUptime = useCallback((): string => { @@ -179,7 +178,6 @@ export default function BentoGrid({ isLive={isLive} isConnected={isConnected} messagesPerMinute={messagesPerMinute} - canModerate={canModerate} /> - + {agent?.did ? ( diff --git a/js/app/src/screens/chat-popout.native.tsx b/js/app/src/screens/chat-popout.native.tsx index 968356e8..d3d806e4 100644 --- a/js/app/src/screens/chat-popout.native.tsx +++ b/js/app/src/screens/chat-popout.native.tsx @@ -171,7 +171,7 @@ export function PopoutChatInner({ user }: { user: string }) { - + {profile && } diff --git a/js/app/src/screens/chat-popout.tsx b/js/app/src/screens/chat-popout.tsx index d7090de9..a20f02c0 100644 --- a/js/app/src/screens/chat-popout.tsx +++ b/js/app/src/screens/chat-popout.tsx @@ -33,6 +33,7 @@ export function PopoutChatInner({ user }: { user: string }) { useEffect(() => { setSrc(user); }, [user]); + return ( - + {profile && } diff --git a/js/components/src/components/chat/chat.tsx b/js/components/src/components/chat/chat.tsx index e99fb064..e517312e 100644 --- a/js/components/src/components/chat/chat.tsx +++ b/js/components/src/components/chat/chat.tsx @@ -136,124 +136,112 @@ const ActionsBar = memo( }, ); -const ChatLine = memo( - ({ - item, - canModerate, - }: { - item: ChatMessageViewHydrated; - canModerate: boolean; - }) => { - const setReply = useSetReplyToMessage(); - const setModMsg = usePlayerStore((state) => state.setModMessage); - const swipeableRef = useRef(null); - const [isHovered, setIsHovered] = useState(false); - const hoverTimeoutRef = useRef(null); +const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => { + const setReply = useSetReplyToMessage(); + const setModMsg = usePlayerStore((state) => state.setModMessage); + const swipeableRef = useRef(null); + const [isHovered, setIsHovered] = useState(false); + const hoverTimeoutRef = useRef(null); - const handleHoverIn = () => { + const handleHoverIn = () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + setIsHovered(true); + }; + + const handleHoverOut = () => { + hoverTimeoutRef.current = setTimeout(() => { + setIsHovered(false); + }, 50); + }; + + useEffect(() => { + return () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; } - setIsHovered(true); - }; - - const handleHoverOut = () => { - hoverTimeoutRef.current = setTimeout(() => { - setIsHovered(false); - }, 50); }; + }, []); - useEffect(() => { - return () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - }; - }, []); - - if (item.author.did === "did:sys:system") { - return ( - - ); - } - - if (Platform.OS === "web") { - return ( - - - - - - - ); - } + if (item.author.did === "did:sys:system") { + return ( + + ); + } + if (Platform.OS === "web") { return ( - <> - { - if (r === (Platform.OS === "android" ? "right" : "left")) { - setReply(item); - } - if (r === (Platform.OS === "android" ? "left" : "right")) { - setModMsg(item); - } - // close this swipeable - const swipeable = swipeableRef.current; - if (swipeable) { - swipeable.close(); - } - }} - > + + - - + + + ); - }, -); + } + + return ( + <> + { + if (r === (Platform.OS === "android" ? "right" : "left")) { + setReply(item); + } + if (r === (Platform.OS === "android" ? "left" : "right")) { + setModMsg(item); + } + // close this swipeable + const swipeable = swipeableRef.current; + if (swipeable) { + swipeable.close(); + } + }} + > + + + + ); +}); export function Chat({ shownMessages = SHOWN_MSGS, style: propsStyle, - canModerate = false, ...props }: ComponentProps & { shownMessages?: number; style?: ComponentProps["style"]; - canModerate?: boolean; }) { const chat = useChat(); const [isScrolledUp, setIsScrolledUp] = useState(false); @@ -276,7 +264,7 @@ export function Chat({ if (!chat) return ( - Loading chaat... + Loading chat... ); @@ -295,9 +283,7 @@ export function Chat({ data={chat.slice(0, shownMessages)} inverted={true} keyExtractor={keyExtractor} - renderItem={({ item, index }) => ( - - )} + renderItem={({ item, index }) => } removeClippedSubviews={true} maxToRenderPerBatch={10} initialNumToRender={10} diff --git a/js/components/src/components/chat/mod-view.tsx b/js/components/src/components/chat/mod-view.tsx index d1453229..7c036d22 100644 --- a/js/components/src/components/chat/mod-view.tsx +++ b/js/components/src/components/chat/mod-view.tsx @@ -1,7 +1,7 @@ import { TriggerRef, useRootContext } from "@rn-primitives/dropdown-menu"; import { forwardRef, useEffect, useRef, useState } from "react"; import { gap, mr, w } from "../../lib/theme/atoms"; -import { usePlayerStore } from "../../player-store"; +import { useIsMyStream, usePlayerStore } from "../../player-store"; import { useCreateBlockRecord, useCreateHideChatRecord, @@ -51,6 +51,7 @@ export const ModView = forwardRef(() => { const setReportSubject = usePlayerStore((x) => x.setReportSubject); const setModMessage = usePlayerStore((x) => x.setModMessage); const deleteChatMessage = useDeleteChatMessage(); + const isMyStream = useIsMyStream(); // get the channel did const channelId = usePlayerStore((state) => state.src); @@ -119,7 +120,7 @@ export const ModView = forwardRef(() => { {/* TODO: Checking for non-owner moderators */} - {channelId === handle && ( + {isMyStream() && ( - + { } return now - Date.parse(segment.startTime) > 10000; }; + +export const useIsMyStream = () => { + const myHandle = useStreamplaceStore((state) => state.handle); + const myDid = useStreamplaceStore((state) => state.oauthSession?.did); + const channelId = usePlayerStore((state) => state.src); + return () => { + return myHandle === channelId || myDid === channelId; + }; +}; -- 2.51.2 From 958e43f748bfeb4f27f64fc22db188ec2739928b Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 15:09:17 -0700 Subject: [PATCH 078/525] rust: bump uniffi --- Makefile | 4 +- pkg/cmd/streamplace.go | 16 - .../iroh_streamplace/iroh_streamplace.go | 435 ++++++------------ .../iroh_streamplace/iroh_streamplace.h | 70 --- pkg/iroh/iroh_streamplace_test.go | 11 +- pkg/media/media_signer.go | 10 +- pkg/media/validate.go | 6 +- pkg/replication/iroh/iroh.go | 7 +- rust/iroh-streamplace/src/sender.rs | 8 +- 9 files changed, 166 insertions(+), 401 deletions(-) diff --git a/Makefile b/Makefile index ecfa3706..21e7dbaf 100644 --- a/Makefile +++ b/Makefile @@ -281,7 +281,9 @@ dev-setup-meson: .PHONY: dev-setup-meson-configure dev-setup-meson-configure: - if ! which uniffi-bindgen-go; then cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.3.0+v0.28.3; fi + if ! which uniffi-bindgen-go >/dev/null 2>&1 || [ "$$(uniffi-bindgen-go --version 2>/dev/null)" != "uniffi-bindgen 0.4.0+v0.28.3" ]; then \ + cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.4.0+v0.28.3; \ + fi meson setup --default-library=shared $(BUILDDIR) $(SHARED_OPTS) meson configure --default-library=shared $(BUILDDIR) $(SHARED_OPTS) diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index d3ad3589..4bc022a3 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -50,22 +50,6 @@ type jobFunc func(ctx context.Context, cli *config.CLI) error // parse the CLI and fire up an streamplace node! func start(build *config.BuildFlags, platformJobs []jobFunc) error { - // builder := c2patypes.Builder{} - // bs, err := os.ReadFile("/home/iameli/.streamplace/segments/did-plc-dkh4rwafdcda4ko7lewe43ml/2025/09/18/20/54/2025-09-18T20-54-31-693Z.mp4") - // if err != nil { - // return err - // } - // manifest, rustErr := iroh_streamplace.GetManifest(bs) - // if rustErr.AsError() != nil { - // return rustErr.AsError() - // } - // var mani c2patypes.Manifest - // err = json.Unmarshal([]byte(manifest), &mani) - // if err != nil { - // return err - // } - // fmt.Println(mani) - // os.Exit(0) selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" err := media.RunSelfTest(context.Background()) if err != nil { diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index a4c5a64b..fae41866 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -609,7 +609,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_nodeaddr_node_id() }) - if checksum != 22953 { + if checksum != 35476 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_nodeaddr_node_id: UniFFI API checksum mismatch") } @@ -650,33 +650,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickey_fmt_short: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_publickeyold_equal() - }) - if checksum != 35079 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_equal: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short() - }) - if checksum != 26426 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes() - }) - if checksum != 41746 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_receiver_node_addr() @@ -690,7 +663,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_receiver_subscribe() }) - if checksum != 46188 { + if checksum != 24145 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_subscribe: UniFFI API checksum mismatch") } @@ -699,7 +672,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe() }) - if checksum != 7879 { + if checksum != 21760 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe: UniFFI API checksum mismatch") } @@ -789,7 +762,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_nodeaddr_new() }) - if checksum != 56594 { + if checksum != 28044 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_nodeaddr_new: UniFFI API checksum mismatch") } @@ -812,24 +785,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickey_from_string: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes() - }) - if checksum != 40751 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string() - }) - if checksum != 54251 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_receiver_new() @@ -843,7 +798,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_sender_new() }) - if checksum != 27594 { + if checksum != 56457 { // If this happens try cleaning and rebuilding your project panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_sender_new: UniFFI API checksum mismatch") } @@ -2312,7 +2267,7 @@ type NodeAddrInterface interface { DirectAddresses() []string // Returns true if both NodeAddr's have the same values Equal(other *NodeAddr) bool - NodeId() *PublicKeyOld + NodeId() *PublicKey // Get the home relay URL for this peer RelayUrl() *string } @@ -2323,9 +2278,9 @@ type NodeAddr struct { } // Create a new [`NodeAddr`] with empty [`AddrInfo`]. -func NewNodeAddr(nodeId *PublicKeyOld, derpUrl *string, addresses []string) *NodeAddr { +func NewNodeAddr(nodeId *PublicKey, derpUrl *string, addresses []string) *NodeAddr { return FfiConverterNodeAddrINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_nodeaddr_new(FfiConverterPublicKeyOldINSTANCE.Lower(nodeId), FfiConverterOptionalStringINSTANCE.Lower(derpUrl), FfiConverterSequenceStringINSTANCE.Lower(addresses), _uniffiStatus) + return C.uniffi_iroh_streamplace_fn_constructor_nodeaddr_new(FfiConverterPublicKeyINSTANCE.Lower(nodeId), FfiConverterOptionalStringINSTANCE.Lower(derpUrl), FfiConverterSequenceStringINSTANCE.Lower(addresses), _uniffiStatus) })) } @@ -2351,10 +2306,10 @@ func (_self *NodeAddr) Equal(other *NodeAddr) bool { })) } -func (_self *NodeAddr) NodeId() *PublicKeyOld { +func (_self *NodeAddr) NodeId() *PublicKey { _pointer := _self.ffiObject.incrementPointer("*NodeAddr") defer _self.ffiObject.decrementPointer() - return FfiConverterPublicKeyOldINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { + return FfiConverterPublicKeyINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { return C.uniffi_iroh_streamplace_fn_method_nodeaddr_node_id( _pointer, _uniffiStatus) })) @@ -2563,155 +2518,12 @@ func (_ FfiDestroyerPublicKey) Destroy(value *PublicKey) { value.Destroy() } -// A public key. -// -// The key itself is just a 32 byte array, but a key has associated crypto -// information that is cached for performance reasons. -type PublicKeyOldInterface interface { - // Returns true if the PublicKeys are equal - Equal(other *PublicKeyOld) bool - // Convert to a base32 string limited to the first 10 bytes for a friendly string - // representation of the key. - FmtShort() string - // Express the PublicKey as a byte array - ToBytes() []byte -} - -// A public key. -// -// The key itself is just a 32 byte array, but a key has associated crypto -// information that is cached for performance reasons. -type PublicKeyOld struct { - ffiObject FfiObject -} - -// Make a PublicKey from byte array -func PublicKeyOldFromBytes(bytes []byte) (*PublicKeyOld, error) { - _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_publickeyold_from_bytes(FfiConverterBytesINSTANCE.Lower(bytes), _uniffiStatus) - }) - if _uniffiErr != nil { - var _uniffiDefaultValue *PublicKeyOld - return _uniffiDefaultValue, _uniffiErr - } else { - return FfiConverterPublicKeyOldINSTANCE.Lift(_uniffiRV), nil - } -} - -// Make a PublicKey from base32 string -func PublicKeyOldFromString(s string) (*PublicKeyOld, error) { - _uniffiRV, _uniffiErr := rustCallWithError[Error](FfiConverterError{}, func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_constructor_publickeyold_from_string(FfiConverterStringINSTANCE.Lower(s), _uniffiStatus) - }) - if _uniffiErr != nil { - var _uniffiDefaultValue *PublicKeyOld - return _uniffiDefaultValue, _uniffiErr - } else { - return FfiConverterPublicKeyOldINSTANCE.Lift(_uniffiRV), nil - } -} - -// Returns true if the PublicKeys are equal -func (_self *PublicKeyOld) Equal(other *PublicKeyOld) bool { - _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") - defer _self.ffiObject.decrementPointer() - return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { - return C.uniffi_iroh_streamplace_fn_method_publickeyold_equal( - _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(other), _uniffiStatus) - })) -} - -// Convert to a base32 string limited to the first 10 bytes for a friendly string -// representation of the key. -func (_self *PublicKeyOld) FmtShort() string { - _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") - defer _self.ffiObject.decrementPointer() - return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_fmt_short( - _pointer, _uniffiStatus), - } - })) -} - -// Express the PublicKey as a byte array -func (_self *PublicKeyOld) ToBytes() []byte { - _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") - defer _self.ffiObject.decrementPointer() - return FfiConverterBytesINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_to_bytes( - _pointer, _uniffiStatus), - } - })) -} - -func (_self *PublicKeyOld) String() string { - _pointer := _self.ffiObject.incrementPointer("*PublicKeyOld") - defer _self.ffiObject.decrementPointer() - return FfiConverterStringINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { - return GoRustBuffer{ - inner: C.uniffi_iroh_streamplace_fn_method_publickeyold_uniffi_trait_display( - _pointer, _uniffiStatus), - } - })) -} - -func (object *PublicKeyOld) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterPublicKeyOld struct{} - -var FfiConverterPublicKeyOldINSTANCE = FfiConverterPublicKeyOld{} - -func (c FfiConverterPublicKeyOld) Lift(pointer unsafe.Pointer) *PublicKeyOld { - result := &PublicKeyOld{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_publickeyold(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_publickeyold(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*PublicKeyOld).Destroy) - return result -} - -func (c FfiConverterPublicKeyOld) Read(reader io.Reader) *PublicKeyOld { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterPublicKeyOld) Lower(value *PublicKeyOld) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*PublicKeyOld") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterPublicKeyOld) Write(writer io.Writer, value *PublicKeyOld) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerPublicKeyOld struct{} - -func (_ FfiDestroyerPublicKeyOld) Destroy(value *PublicKeyOld) { - value.Destroy() -} - type ReceiverInterface interface { NodeAddr() *NodeAddr // Subscribe to the given topic on the remote. - Subscribe(remoteId *PublicKeyOld, topic string) error + Subscribe(remoteId *PublicKey, topic string) error // Unsubscribe from this topic on the remote. - Unsubscribe(remoteId *PublicKeyOld, topic string) error + Unsubscribe(remoteId *PublicKey, topic string) error } type Receiver struct { ffiObject FfiObject @@ -2774,7 +2586,7 @@ func (_self *Receiver) NodeAddr() *NodeAddr { } // Subscribe to the given topic on the remote. -func (_self *Receiver) Subscribe(remoteId *PublicKeyOld, topic string) error { +func (_self *Receiver) Subscribe(remoteId *PublicKey, topic string) error { _pointer := _self.ffiObject.incrementPointer("*Receiver") defer _self.ffiObject.decrementPointer() _, err := uniffiRustCallAsync[Error]( @@ -2787,7 +2599,7 @@ func (_self *Receiver) Subscribe(remoteId *PublicKeyOld, topic string) error { // liftFn func(_ struct{}) struct{} { return struct{}{} }, C.uniffi_iroh_streamplace_fn_method_receiver_subscribe( - _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), + _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) @@ -2802,7 +2614,7 @@ func (_self *Receiver) Subscribe(remoteId *PublicKeyOld, topic string) error { } // Unsubscribe from this topic on the remote. -func (_self *Receiver) Unsubscribe(remoteId *PublicKeyOld, topic string) error { +func (_self *Receiver) Unsubscribe(remoteId *PublicKey, topic string) error { _pointer := _self.ffiObject.incrementPointer("*Receiver") defer _self.ffiObject.decrementPointer() _, err := uniffiRustCallAsync[Error]( @@ -2815,7 +2627,7 @@ func (_self *Receiver) Unsubscribe(remoteId *PublicKeyOld, topic string) error { // liftFn func(_ struct{}) struct{} { return struct{}{} }, C.uniffi_iroh_streamplace_fn_method_receiver_unsubscribe( - _pointer, FfiConverterPublicKeyOldINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), + _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), // pollFn func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) @@ -2887,9 +2699,9 @@ type Sender struct { } // Create a new sender. -func NewSender(endpoint *Endpoint) (*Sender, error) { - res, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, +func NewSender(endpoint *Endpoint) *Sender { + res, _ := uniffiRustCallAsync[error]( + nil, // completeFn func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) @@ -2910,7 +2722,7 @@ func NewSender(endpoint *Endpoint) (*Sender, error) { }, ) - return res, err + return res } func (_self *Sender) NodeAddr() *NodeAddr { @@ -3616,11 +3428,8 @@ func (err Error) Unwrap() error { // Err* are used for checking error type with `errors.Is` var ErrErrorIrohBind = fmt.Errorf("ErrorIrohBind") -var ErrErrorInvalidUrl = fmt.Errorf("ErrorInvalidUrl") var ErrErrorIrohConnect = fmt.Errorf("ErrorIrohConnect") -var ErrErrorInvalidNetworkAddress = fmt.Errorf("ErrorInvalidNetworkAddress") var ErrErrorMissingConnection = fmt.Errorf("ErrorMissingConnection") -var ErrErrorInvalidPublicKey = fmt.Errorf("ErrorInvalidPublicKey") var ErrErrorIrpc = fmt.Errorf("ErrorIrpc") // Variant structs @@ -3643,25 +3452,6 @@ func (self ErrorIrohBind) Is(target error) bool { return target == ErrErrorIrohBind } -type ErrorInvalidUrl struct { - message string -} - -func NewErrorInvalidUrl() *Error { - return &Error{err: &ErrorInvalidUrl{}} -} - -func (e ErrorInvalidUrl) destroy() { -} - -func (err ErrorInvalidUrl) Error() string { - return fmt.Sprintf("InvalidUrl: %s", err.message) -} - -func (self ErrorInvalidUrl) Is(target error) bool { - return target == ErrErrorInvalidUrl -} - type ErrorIrohConnect struct { message string } @@ -3681,25 +3471,6 @@ func (self ErrorIrohConnect) Is(target error) bool { return target == ErrErrorIrohConnect } -type ErrorInvalidNetworkAddress struct { - message string -} - -func NewErrorInvalidNetworkAddress() *Error { - return &Error{err: &ErrorInvalidNetworkAddress{}} -} - -func (e ErrorInvalidNetworkAddress) destroy() { -} - -func (err ErrorInvalidNetworkAddress) Error() string { - return fmt.Sprintf("InvalidNetworkAddress: %s", err.message) -} - -func (self ErrorInvalidNetworkAddress) Is(target error) bool { - return target == ErrErrorInvalidNetworkAddress -} - type ErrorMissingConnection struct { message string } @@ -3719,25 +3490,6 @@ func (self ErrorMissingConnection) Is(target error) bool { return target == ErrErrorMissingConnection } -type ErrorInvalidPublicKey struct { - message string -} - -func NewErrorInvalidPublicKey() *Error { - return &Error{err: &ErrorInvalidPublicKey{}} -} - -func (e ErrorInvalidPublicKey) destroy() { -} - -func (err ErrorInvalidPublicKey) Error() string { - return fmt.Sprintf("InvalidPublicKey: %s", err.message) -} - -func (self ErrorInvalidPublicKey) Is(target error) bool { - return target == ErrErrorInvalidPublicKey -} - type ErrorIrpc struct { message string } @@ -3777,16 +3529,10 @@ func (c FfiConverterError) Read(reader io.Reader) *Error { case 1: return &Error{&ErrorIrohBind{message}} case 2: - return &Error{&ErrorInvalidUrl{message}} - case 3: return &Error{&ErrorIrohConnect{message}} - case 4: - return &Error{&ErrorInvalidNetworkAddress{message}} - case 5: + case 3: return &Error{&ErrorMissingConnection{message}} - case 6: - return &Error{&ErrorInvalidPublicKey{message}} - case 7: + case 4: return &Error{&ErrorIrpc{message}} default: panic(fmt.Sprintf("Unknown error code %d in FfiConverterError.Read()", errorID)) @@ -3798,18 +3544,12 @@ func (c FfiConverterError) Write(writer io.Writer, value *Error) { switch variantValue := value.err.(type) { case *ErrorIrohBind: writeInt32(writer, 1) - case *ErrorInvalidUrl: - writeInt32(writer, 2) case *ErrorIrohConnect: - writeInt32(writer, 3) - case *ErrorInvalidNetworkAddress: - writeInt32(writer, 4) + writeInt32(writer, 2) case *ErrorMissingConnection: - writeInt32(writer, 5) - case *ErrorInvalidPublicKey: - writeInt32(writer, 6) + writeInt32(writer, 3) case *ErrorIrpc: - writeInt32(writer, 7) + writeInt32(writer, 4) default: _ = variantValue panic(fmt.Sprintf("invalid error value `%v` in FfiConverterError.Write", value)) @@ -3822,16 +3562,10 @@ func (_ FfiDestroyerError) Destroy(value *Error) { switch variantValue := value.err.(type) { case ErrorIrohBind: variantValue.destroy() - case ErrorInvalidUrl: - variantValue.destroy() case ErrorIrohConnect: variantValue.destroy() - case ErrorInvalidNetworkAddress: - variantValue.destroy() case ErrorMissingConnection: variantValue.destroy() - case ErrorInvalidPublicKey: - variantValue.destroy() case ErrorIrpc: variantValue.destroy() default: @@ -3985,6 +3719,125 @@ func (_ FfiDestroyerJoinPeersError) Destroy(value *JoinPeersError) { } } +// Error when converting from ffi NodeAddr to iroh::NodeAddr +type NodeAddrError struct { + err error +} + +// Convience method to turn *NodeAddrError into error +// Avoiding treating nil pointer as non nil error interface +func (err *NodeAddrError) AsError() error { + if err == nil { + return nil + } else { + return err + } +} + +func (err NodeAddrError) Error() string { + return fmt.Sprintf("NodeAddrError: %s", err.err.Error()) +} + +func (err NodeAddrError) Unwrap() error { + return err.err +} + +// Err* are used for checking error type with `errors.Is` +var ErrNodeAddrErrorInvalidUrl = fmt.Errorf("NodeAddrErrorInvalidUrl") +var ErrNodeAddrErrorInvalidNetworkAddress = fmt.Errorf("NodeAddrErrorInvalidNetworkAddress") + +// Variant structs +type NodeAddrErrorInvalidUrl struct { + message string +} + +func NewNodeAddrErrorInvalidUrl() *NodeAddrError { + return &NodeAddrError{err: &NodeAddrErrorInvalidUrl{}} +} + +func (e NodeAddrErrorInvalidUrl) destroy() { +} + +func (err NodeAddrErrorInvalidUrl) Error() string { + return fmt.Sprintf("InvalidUrl: %s", err.message) +} + +func (self NodeAddrErrorInvalidUrl) Is(target error) bool { + return target == ErrNodeAddrErrorInvalidUrl +} + +type NodeAddrErrorInvalidNetworkAddress struct { + message string +} + +func NewNodeAddrErrorInvalidNetworkAddress() *NodeAddrError { + return &NodeAddrError{err: &NodeAddrErrorInvalidNetworkAddress{}} +} + +func (e NodeAddrErrorInvalidNetworkAddress) destroy() { +} + +func (err NodeAddrErrorInvalidNetworkAddress) Error() string { + return fmt.Sprintf("InvalidNetworkAddress: %s", err.message) +} + +func (self NodeAddrErrorInvalidNetworkAddress) Is(target error) bool { + return target == ErrNodeAddrErrorInvalidNetworkAddress +} + +type FfiConverterNodeAddrError struct{} + +var FfiConverterNodeAddrErrorINSTANCE = FfiConverterNodeAddrError{} + +func (c FfiConverterNodeAddrError) Lift(eb RustBufferI) *NodeAddrError { + return LiftFromRustBuffer[*NodeAddrError](c, eb) +} + +func (c FfiConverterNodeAddrError) Lower(value *NodeAddrError) C.RustBuffer { + return LowerIntoRustBuffer[*NodeAddrError](c, value) +} + +func (c FfiConverterNodeAddrError) Read(reader io.Reader) *NodeAddrError { + errorID := readUint32(reader) + + message := FfiConverterStringINSTANCE.Read(reader) + switch errorID { + case 1: + return &NodeAddrError{&NodeAddrErrorInvalidUrl{message}} + case 2: + return &NodeAddrError{&NodeAddrErrorInvalidNetworkAddress{message}} + default: + panic(fmt.Sprintf("Unknown error code %d in FfiConverterNodeAddrError.Read()", errorID)) + } + +} + +func (c FfiConverterNodeAddrError) Write(writer io.Writer, value *NodeAddrError) { + switch variantValue := value.err.(type) { + case *NodeAddrErrorInvalidUrl: + writeInt32(writer, 1) + case *NodeAddrErrorInvalidNetworkAddress: + writeInt32(writer, 2) + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiConverterNodeAddrError.Write", value)) + } +} + +type FfiDestroyerNodeAddrError struct{} + +func (_ FfiDestroyerNodeAddrError) Destroy(value *NodeAddrError) { + switch variantValue := value.err.(type) { + case NodeAddrErrorInvalidUrl: + variantValue.destroy() + case NodeAddrErrorInvalidNetworkAddress: + variantValue.destroy() + default: + _ = variantValue + panic(fmt.Sprintf("invalid error value `%v` in FfiDestroyerNodeAddrError.Destroy", value)) + } +} + type PublicKeyError struct { err error } diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index b4fecb00..49e04249 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -746,46 +746,6 @@ RustBuffer uniffi_iroh_streamplace_fn_method_publickey_fmt_short(void* ptr, Rust RustBuffer uniffi_iroh_streamplace_fn_method_publickey_uniffi_trait_display(void* ptr, RustCallStatus *out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_PUBLICKEYOLD -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_PUBLICKEYOLD -void* uniffi_iroh_streamplace_fn_clone_publickeyold(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_PUBLICKEYOLD -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_PUBLICKEYOLD -void uniffi_iroh_streamplace_fn_free_publickeyold(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES -void* uniffi_iroh_streamplace_fn_constructor_publickeyold_from_bytes(RustBuffer bytes, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING -void* uniffi_iroh_streamplace_fn_constructor_publickeyold_from_string(RustBuffer s, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_EQUAL -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_EQUAL -int8_t uniffi_iroh_streamplace_fn_method_publickeyold_equal(void* ptr, void* other, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_FMT_SHORT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_FMT_SHORT -RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_fmt_short(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_TO_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_TO_BYTES -RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_to_bytes(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_UNIFFI_TRAIT_DISPLAY -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_PUBLICKEYOLD_UNIFFI_TRAIT_DISPLAY -RustBuffer uniffi_iroh_streamplace_fn_method_publickeyold_uniffi_trait_display(void* ptr, RustCallStatus *out_status -); -#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER void* uniffi_iroh_streamplace_fn_clone_receiver(void* ptr, RustCallStatus *out_status @@ -1367,24 +1327,6 @@ uint16_t uniffi_iroh_streamplace_checksum_method_publickey_equal(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_FMT_SHORT uint16_t uniffi_iroh_streamplace_checksum_method_publickey_fmt_short(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_EQUAL -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_EQUAL -uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_equal(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_FMT_SHORT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_FMT_SHORT -uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_fmt_short(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_TO_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEYOLD_TO_BYTES -uint16_t uniffi_iroh_streamplace_checksum_method_publickeyold_to_bytes(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_NODE_ADDR @@ -1475,18 +1417,6 @@ uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_bytes(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEY_FROM_STRING uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_string(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_BYTES -uint16_t uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_bytes(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEYOLD_FROM_STRING -uint16_t uniffi_iroh_streamplace_checksum_constructor_publickeyold_from_string(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_RECEIVER_NEW diff --git a/pkg/iroh/iroh_streamplace_test.go b/pkg/iroh/iroh_streamplace_test.go index 1e89d501..41736876 100644 --- a/pkg/iroh/iroh_streamplace_test.go +++ b/pkg/iroh/iroh_streamplace_test.go @@ -24,28 +24,27 @@ func (handler TestHandler) HandleData(topic string, data []byte) { func TestBasicRoundtrip(t *testing.T) { ep1, err := iroh.NewEndpoint() assert.Nil(t, err) - sender, err := iroh.NewSender(ep1) - assert.NoError(t, err.AsError()) + sender := iroh.NewSender(ep1) messages := make(chan Message, 5) handler := TestHandler{messages: messages} ep2, err := iroh.NewEndpoint() - assert.NoError(t, err.AsError()) + assert.NoError(t, err) receiver, err := iroh.NewReceiver(ep2, &handler) - assert.NoError(t, err.AsError()) + assert.NoError(t, err) senderAddr := sender.NodeAddr() senderId := senderAddr.NodeId() // subscribe err = receiver.Subscribe(senderId, "foo") - assert.NoError(t, err.AsError()) + assert.NoError(t, err) // send a few messages for i := range 5 { err = sender.Send("foo", []byte{byte(i), 0, 0, 0}) - assert.NoError(t, err.AsError()) + assert.NoError(t, err) } // make sure the receiver got them diff --git a/pkg/media/media_signer.go b/pkg/media/media_signer.go index e49afae6..59ac66ae 100644 --- a/pkg/media/media_signer.go +++ b/pkg/media/media_signer.go @@ -125,9 +125,9 @@ func (ms *MediaSignerLocal) SignMP4(ctx context.Context, input io.ReadSeeker, st rustCallbackSigner := &RustCallbackSigner{ Signer: ms.Signer, } - bs, rustErr := iroh_streamplace.Sign(string(manifestBs), bs, ms.Cert, rustCallbackSigner) - if rustErr.AsError() != nil { - return nil, rustErr.AsError() + bs, err = iroh_streamplace.Sign(string(manifestBs), bs, ms.Cert, rustCallbackSigner) + if err != nil { + return nil, err } span.End() @@ -153,11 +153,11 @@ type RustCallbackSigner struct { Signer crypto.Signer } -func (rcs *RustCallbackSigner) Sign(data []byte) ([]byte, *iroh_streamplace.SpError) { +func (rcs *RustCallbackSigner) Sign(data []byte) ([]byte, error) { digest := sha256.Sum256(data) sig, err := rcs.Signer.Sign(rand.Reader, digest[:], nil) if err != nil { - return nil, iroh_streamplace.NewSpErrorNoCertificateChainFound() + return nil, fmt.Errorf("failed to sign data: %w", err) } return sig, nil } diff --git a/pkg/media/validate.go b/pkg/media/validate.go index a5f027a5..df17c39a 100644 --- a/pkg/media/validate.go +++ b/pkg/media/validate.go @@ -31,9 +31,9 @@ func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error return err } var maniCert ManifestAndCert - maniStr, rustErr := iroh_streamplace.GetManifestAndCert(buf) - if rustErr.AsError() != nil { - return rustErr.AsError() + maniStr, err := iroh_streamplace.GetManifestAndCert(buf) + if err != nil { + return err } err = json.Unmarshal([]byte(maniStr), &maniCert) if err != nil { diff --git a/pkg/replication/iroh/iroh.go b/pkg/replication/iroh/iroh.go index 9f5d4547..6c45b548 100644 --- a/pkg/replication/iroh/iroh.go +++ b/pkg/replication/iroh/iroh.go @@ -15,10 +15,7 @@ type IrohReplicator struct { } func NewIrohReplicator(ctx context.Context, ep *irohStreamplace.Endpoint, topic string) (*IrohReplicator, error) { - sender, err := irohStreamplace.NewSender(ep) - if err.AsError() != nil { - return nil, err.AsError() - } + sender := irohStreamplace.NewSender(ep) return &IrohReplicator{ topic: topic, @@ -36,5 +33,5 @@ func (rep *IrohReplicator) NewSegment(ctx context.Context, bs []byte) { } func sendSegment(endpoint *irohStreamplace.Sender, topic string, bs []byte) error { - return endpoint.Send(topic, bs).AsError() + return endpoint.Send(topic, bs) } diff --git a/rust/iroh-streamplace/src/sender.rs b/rust/iroh-streamplace/src/sender.rs index 15cfa34d..9b665a7f 100644 --- a/rust/iroh-streamplace/src/sender.rs +++ b/rust/iroh-streamplace/src/sender.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use iroh::protocol::Router; -use crate::{api::Api, endpoint::Endpoint, error::Error, node_addr::NodeAddr}; +use crate::{api::Api, c2pa::SPError, endpoint::Endpoint, error::Error, node_addr::NodeAddr}; #[derive(uniffi::Object)] pub struct Sender { @@ -14,17 +14,17 @@ pub struct Sender { impl Sender { /// Create a new sender. #[uniffi::constructor(async_runtime = "tokio")] - pub async fn new(endpoint: &Endpoint) -> Result { + pub async fn new(endpoint: &Endpoint) -> Sender { let api = Api::spawn(&endpoint.endpoint); let router = Router::builder(endpoint.endpoint.clone()) .accept(Api::ALPN, api.expose()) .spawn(); - Ok(Sender { + Sender { endpoint: endpoint.clone(), api, _router: router, - }) + } } /// Sends the given data to all subscribers that have subscribed to this `key`. -- 2.51.2 From 504fd255fcd431dd7414f210cf91a0bc6d2a5bdb Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 15:21:12 -0700 Subject: [PATCH 079/525] go: bump to 1.25.1 --- .ci/dockerfile-hash.yaml | 2 +- docker/build.Dockerfile | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/dockerfile-hash.yaml b/.ci/dockerfile-hash.yaml index cd63f7c8..160304ab 100644 --- a/.ci/dockerfile-hash.yaml +++ b/.ci/dockerfile-hash.yaml @@ -1,2 +1,2 @@ variables: - DOCKERFILE_HASH: ba6d86b3cd6b9fdcbe238b6f4e454d54e07a0d48 \ No newline at end of file + DOCKERFILE_HASH: a0cc55e6b9dc095726690e60bec764a94031d610 \ No newline at end of file diff --git a/docker/build.Dockerfile b/docker/build.Dockerfile index ba6d86b3..a0cc55e6 100644 --- a/docker/build.Dockerfile +++ b/docker/build.Dockerfile @@ -7,7 +7,7 @@ ENV TARGETARCH $TARGETARCH ARG DOCKERFILE_HASH ENV DOCKERFILE_HASH $DOCKERFILE_HASH -ENV GO_VERSION 1.24.2 +ENV GO_VERSION 1.25.1 ENV NODE_VERSION 22.15.0 ENV DEBIAN_FRONTEND noninteractive diff --git a/go.mod b/go.mod index 16bbd390..655da100 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module stream.place/streamplace -go 1.24.2 +go 1.25.1 replace github.com/ThalesGroup/crypto11 => github.com/aquareum-tv/crypto11 v0.0.0-20240821184406-43336abc768f -- 2.51.2 From 03d34c604ca094da4e6b7ef845e2f3e0199a363b Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 17:02:32 -0700 Subject: [PATCH 080/525] iroh: working KV replication! --- .vscode/settings.json | 4 +- pkg/cmd/streamplace.go | 35 ++++++- pkg/config/config.go | 2 + pkg/director/director.go | 12 ++- .../iroh_streamplace/iroh_streamplace.go | 68 +++++++++++++ .../{iroh => iroh_replicator}/iroh.go | 2 +- pkg/replication/iroh_replicator/kv.go | 99 +++++++++++++++++++ rust/iroh-streamplace/Makefile | 19 ---- rust/iroh-streamplace/go/go.mod | 3 - rust/iroh-streamplace/go/main.go | 95 ------------------ 10 files changed, 217 insertions(+), 122 deletions(-) rename pkg/replication/{iroh => iroh_replicator}/iroh.go (97%) create mode 100644 pkg/replication/iroh_replicator/kv.go delete mode 100644 rust/iroh-streamplace/Makefile delete mode 100644 rust/iroh-streamplace/go/go.mod delete mode 100644 rust/iroh-streamplace/go/main.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d459a8b..076ee90f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,11 +10,11 @@ }, "mesonbuild.configureOnOpen": false, "cSpell.words": ["Devplace", "streamplace", "webrtc"], - "go.lintTool": "golangci-lint", + "go.lintTool": "golangci-lint-v2", "go.lintFlags": ["--path-mode=abs"], "go.formatTool": "custom", "go.alternateTools": { - "customFormatter": "golangci-lint" + "customFormatter": "golangci-lint-v2" }, "go.formatFlags": ["fmt", "--stdin"], "editor.codeActionsOnSave": { diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 4bc022a3..6501bd33 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -1,8 +1,10 @@ package cmd import ( + "bytes" "context" "crypto" + "crypto/rand" "errors" "flag" "fmt" @@ -32,6 +34,7 @@ import ( "stream.place/streamplace/pkg/notifications" "stream.place/streamplace/pkg/replication" "stream.place/streamplace/pkg/replication/boring" + "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/rtmps" v0 "stream.place/streamplace/pkg/schema/v0" "stream.place/streamplace/pkg/spmetrics" @@ -379,6 +382,32 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { }, } + exists, err := cli.DataFileExists([]string{"iroh-kv-secret"}) + if err != nil { + return err + } + if !exists { + secret := make([]byte, 32) + _, err := rand.Read(secret) + if err != nil { + return fmt.Errorf("failed to generate random secret: %w", err) + } + err = cli.DataFileWrite([]string{"iroh-kv-secret"}, bytes.NewReader(secret), true) + if err != nil { + return err + } + } + buf := bytes.Buffer{} + err = cli.DataFileRead([]string{"iroh-kv-secret"}, &buf) + if err != nil { + return err + } + secret := buf.Bytes() + swarm, err := iroh_replicator.StartKV(ctx, cli.Tickets, secret) + if err != nil { + return err + } + op := oatproxy.New(&oatproxy.Config{ Host: cli.PublicHost, CreateOAuthSession: state.CreateOAuthSession, @@ -390,7 +419,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { DownstreamJWK: cli.AccessJWK, ClientMetadata: clientMetadata, }) - d := director.NewDirector(mm, mod, &cli, b, op, state) + d := director.NewDirector(mm, mod, &cli, b, op, state, swarm) a, err := api.MakeStreamplaceAPI(&cli, mod, state, eip712signer, noter, mm, ms, b, atsync, d, op) if err != nil { return err @@ -453,6 +482,10 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { return mod.StartSegmentCleaner(ctx) }) + group.Go(func() error { + return swarm.Start(ctx, cli.Tickets) + }) + if cli.LivepeerGateway { // make a file to make sure the directory exists fd, err := cli.DataFileCreate([]string{"livepeer", "gateway", "empty"}, true) diff --git a/pkg/config/config.go b/pkg/config/config.go index f692e80d..b71f54d3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -118,6 +118,7 @@ type CLI struct { SQLLogging bool SentryDSN string LivepeerDebug bool + Tickets []string } func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { @@ -184,6 +185,7 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { fs.BoolVar(&cli.SQLLogging, "sql-logging", false, "enable sql logging") fs.StringVar(&cli.SentryDSN, "sentry-dsn", "", "sentry dsn for error reporting") fs.BoolVar(&cli.LivepeerDebug, "livepeer-debug", false, "log livepeer segments to $SP_DATA_DIR/livepeer-debug") + cli.StringSliceFlag(fs, &cli.Tickets, "tickets", "[]", "tickets to join the swarm with") lpFlags := flag.NewFlagSet("livepeer", flag.ContinueOnError) _ = starter.NewLivepeerConfig(lpFlags) diff --git a/pkg/director/director.go b/pkg/director/director.go index e260250f..3ecf05e8 100644 --- a/pkg/director/director.go +++ b/pkg/director/director.go @@ -5,6 +5,7 @@ import ( "fmt" "sync" + "github.com/bluesky-social/indigo/util" "github.com/streamplace/oatproxy/pkg/oatproxy" "golang.org/x/sync/errgroup" "stream.place/streamplace/pkg/bus" @@ -12,6 +13,7 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/model" + "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/statedb" ) @@ -30,9 +32,10 @@ type Director struct { streamSessionsMu sync.Mutex op *oatproxy.OATProxy statefulDB *statedb.StatefulDB + swarm *iroh_replicator.SwarmKV } -func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB) *Director { +func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB, swarm *iroh_replicator.SwarmKV) *Director { return &Director{ mm: mm, mod: mod, @@ -42,6 +45,7 @@ func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus * streamSessionsMu: sync.Mutex{}, op: op, statefulDB: statefulDB, + swarm: swarm, } } @@ -86,6 +90,12 @@ func (d *Director) Start(ctx context.Context) error { }) } d.streamSessionsMu.Unlock() + go func() { + err := d.swarm.Put(ctx, not.Segment.RepoDID, not.Segment.StartTime.Format(util.ISO8601)) + if err != nil { + log.Error(ctx, "could not put segment to swarm", "error", err) + } + }() err := ss.NewSegment(ctx, not) if err != nil { log.Error(ctx, "could not add segment to stream session", "error", err) diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index fae41866..e4125b08 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -1440,6 +1440,10 @@ func (_self *Db) IterWithOpts(filter *Filter) ([]Entry, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -1556,6 +1560,10 @@ func NewEndpoint() (*Endpoint, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -1961,6 +1969,10 @@ func NodeForwarder(config Config) (*Node, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -1987,6 +1999,10 @@ func NodeReceiver(config Config, handler DataHandler) (*Node, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2014,6 +2030,10 @@ func NodeSender(config Config) (*Node, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2052,6 +2072,10 @@ func (_self *Node) JoinPeers(peers []string) error { }, ) + if err == nil { + return nil + } + return err } @@ -2082,6 +2106,10 @@ func (_self *Node) NodeId() (*PublicKey, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2122,6 +2150,10 @@ func (_self *Node) SendSegment(key string, data []byte) error { }, ) + if err == nil { + return nil + } + return err } @@ -2150,6 +2182,10 @@ func (_self *Node) Subscribe(key string, remoteId *PublicKey) error { }, ) + if err == nil { + return nil + } + return err } @@ -2182,6 +2218,10 @@ func (_self *Node) Ticket() (string, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2210,6 +2250,10 @@ func (_self *Node) Unsubscribe(key string, remoteId *PublicKey) error { }, ) + if err == nil { + return nil + } + return err } func (object *Node) Destroy() { @@ -2553,6 +2597,10 @@ func NewReceiver(endpoint *Endpoint, handler DataHandlerOld) (*Receiver, error) }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2610,6 +2658,10 @@ func (_self *Receiver) Subscribe(remoteId *PublicKey, topic string) error { }, ) + if err == nil { + return nil + } + return err } @@ -2638,6 +2690,10 @@ func (_self *Receiver) Unsubscribe(remoteId *PublicKey, topic string) error { }, ) + if err == nil { + return nil + } + return err } func (object *Receiver) Destroy() { @@ -2779,6 +2835,10 @@ func (_self *Sender) Send(key string, data []byte) error { }, ) + if err == nil { + return nil + } + return err } func (object *Sender) Destroy() { @@ -2872,6 +2932,10 @@ func (_self *SubscribeResponse) NextRaw() (*SubscribeItem, error) { }, ) + if err == nil { + return res, nil + } + return res, err } @@ -2973,6 +3037,10 @@ func (_self *WriteScope) Put(stream *[]byte, key []byte, value []byte) error { }, ) + if err == nil { + return nil + } + return err } func (object *WriteScope) Destroy() { diff --git a/pkg/replication/iroh/iroh.go b/pkg/replication/iroh_replicator/iroh.go similarity index 97% rename from pkg/replication/iroh/iroh.go rename to pkg/replication/iroh_replicator/iroh.go index 6c45b548..138ab0ea 100644 --- a/pkg/replication/iroh/iroh.go +++ b/pkg/replication/iroh_replicator/iroh.go @@ -1,4 +1,4 @@ -package iroh +package iroh_replicator import ( "context" diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go new file mode 100644 index 00000000..68e63713 --- /dev/null +++ b/pkg/replication/iroh_replicator/kv.go @@ -0,0 +1,99 @@ +package iroh_replicator + +import ( + "context" + "fmt" + "time" + + "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" + "stream.place/streamplace/pkg/log" +) + +type SwarmKV struct { + node *iroh_streamplace.Node + db *iroh_streamplace.Db + w *iroh_streamplace.WriteScope +} + +func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, error) { + ctx = log.WithLogValues(ctx, "func", "StartKV") + + log.Log(ctx, "Starting with tickets", "tickets", tickets) + config := iroh_streamplace.Config{ + Key: secret, + Topic: make([]byte, 32), // all zero topic for testing + MaxSendDuration: 1000_000_000, // 1s + } + log.Log(ctx, "Config created", "config", config) + node, err := iroh_streamplace.NodeSender(config) + if err != nil { + return nil, fmt.Errorf("failed to create NodeSender: %w", err) + } + + db := node.Db() + w := node.NodeScope() + + node_id, err := node.NodeId() + if err != nil { + return nil, fmt.Errorf("failed to get NodeId: %w", err) + } + log.Log(ctx, "Node ID:", "node_id", node_id) + + ticket, err := node.Ticket() + if err != nil { + return nil, fmt.Errorf("failed to get Ticket: %w", err) + } + log.Log(ctx, "Ticket:", "ticket", ticket) + + swarm := SwarmKV{ + node: node, + db: db, + w: w, + } + return &swarm, nil +} + +func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { + if len(tickets) > 0 { + err := swarm.node.JoinPeers(tickets) + if err != nil { + return fmt.Errorf("failed to join peers: %w", err) + } + } + + sub := swarm.db.Subscribe(iroh_streamplace.NewFilter()) + for { + if ctx.Err() != nil { + return ctx.Err() + } + ev, err := sub.NextRaw() + if err != nil { + return fmt.Errorf("failed to get next subscription event: %w", err) + } + if ev == nil { + log.Log(ctx, "Got empty event from sub.NextRaw(), pausing for a second") + time.Sleep(1 * time.Second) + continue + } + switch item := (*ev).(type) { + case iroh_streamplace.SubscribeItemEntry: + keyStr := string(item.Key) + valueStr := string(item.Value) + log.Log(ctx, "SubscribeItemEntry", "key", keyStr, "value", valueStr) + + case iroh_streamplace.SubscribeItemCurrentDone: + log.Log(ctx, "SubscribeItemCurrentDone", "currentDone", item) + case iroh_streamplace.SubscribeItemExpired: + log.Log(ctx, "SubscribeItemExpired", "expired", item) + case iroh_streamplace.SubscribeItemOther: + log.Log(ctx, "SubscribeItemOther", "other", item) + } + } +} + +func (swarm *SwarmKV) Put(ctx context.Context, key, value string) error { + // streamerBs := []byte(streamer) + keyBs := []byte(key) + valueBs := []byte(value) + return swarm.w.Put(nil, keyBs, valueBs) +} diff --git a/rust/iroh-streamplace/Makefile b/rust/iroh-streamplace/Makefile deleted file mode 100644 index cf684ec0..00000000 --- a/rust/iroh-streamplace/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -.PHONY: clean build-rust generate run - -install: - cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.4.0+v0.28.3 - -clean: - cargo clean - rm -rf go/uniffi_example/ - -build-rust: - cargo build - -generate: build-rust - uniffi-bindgen-go --library ../../target/debug/libiroh_streamplace.dylib --out-dir go/ - cp ../../target/debug/libiroh_streamplace.dylib go/ - -run: generate - cd go && CGO_LDFLAGS="-L. -liroh_streamplace" go run main.go $(TICKET) - diff --git a/rust/iroh-streamplace/go/go.mod b/rust/iroh-streamplace/go/go.mod deleted file mode 100644 index 4a93f614..00000000 --- a/rust/iroh-streamplace/go/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module hello-app - -go 1.25.1 diff --git a/rust/iroh-streamplace/go/main.go b/rust/iroh-streamplace/go/main.go deleted file mode 100644 index 70e2ec3c..00000000 --- a/rust/iroh-streamplace/go/main.go +++ /dev/null @@ -1,95 +0,0 @@ - -package main - -import ( - "fmt" - "os" - sp "hello-app/iroh_streamplace" - "reflect" - "crypto/rand" -) - -func isNilError(err error) bool { - if err == nil { - return true - } - - v := reflect.ValueOf(err) - return v.Kind() == reflect.Ptr && v.IsNil() -} - - -func panicIfErr(err error) { - if !isNilError(err) { - panic(err) - } -} - -func main() { - tickets := os.Args[1:]; - - secret := make([]byte, 32) - _, err := rand.Read(secret) - panicIfErr(err) - - fmt.Println("Starting with tickets", tickets) - config := sp.Config { - Key : secret, - Topic: make([]byte, 32), // all zero topic for testing - MaxSendDuration: 1000_000_000, // 1s - } - fmt.Printf("Config created %+v\n", config) - node, err := sp.NodeSender(config) - panicIfErr(err) - - db := node.Db() - w := node.NodeScope() - - node_id, err := node.NodeId() - panicIfErr(err) - fmt.Println("Node ID:", node_id) - - ticket, err := node.Ticket() - panicIfErr(err) - fmt.Println("Ticket:", ticket) - - if len(tickets) > 0 { - err = node.JoinPeers(tickets) - panicIfErr(err) - } - - w.Put(nil, []byte("hello"), []byte("world")) - stream := []byte("stream1") - w.Put(&stream, []byte("subscribed"), []byte("true")) - - filter := sp.NewFilter() - items, err := db.IterWithOpts(filter) - panicIfErr(err) - fmt.Printf("Iter items: %+v\n", items) - - filter2 := sp.NewFilter().Global() - items2, err := db.IterWithOpts(filter2) - panicIfErr(err) - fmt.Printf("Iter items: %+v\n", items2) - - filter3 := sp.NewFilter().Stream(stream) - items3, err := db.IterWithOpts(filter3) - panicIfErr(err) - fmt.Printf("Iter items: %+v\n", items3) - - sub := db.Subscribe(sp.NewFilter()) - for { - ev, err := sub.NextRaw() - panicIfErr(err) - switch (*ev).(type) { - case sp.SubscribeItemEntry: - fmt.Printf("%+v\n", (*ev).(sp.SubscribeItemEntry)) - case sp.SubscribeItemCurrentDone: - fmt.Printf("Got current done event: %+v\n", (*ev).(sp.SubscribeItemCurrentDone)) - case sp.SubscribeItemExpired: - fmt.Printf("Got expired event: %+v\n", (*ev).(sp.SubscribeItemExpired)) - case sp.SubscribeItemOther: - fmt.Printf("Got other event: %+v\n", (*ev).(sp.SubscribeItemOther)) - } - } -} \ No newline at end of file -- 2.51.2 From 53530d48357a2fbd5ea4f0a012d203064428ae83 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 17:18:09 -0700 Subject: [PATCH 081/525] Makefile: download forked uniffi-bindgen-go --- Makefile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 21e7dbaf..4c170f8f 100644 --- a/Makefile +++ b/Makefile @@ -281,18 +281,15 @@ dev-setup-meson: .PHONY: dev-setup-meson-configure dev-setup-meson-configure: - if ! which uniffi-bindgen-go >/dev/null 2>&1 || [ "$$(uniffi-bindgen-go --version 2>/dev/null)" != "uniffi-bindgen 0.4.0+v0.28.3" ]; then \ - cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.4.0+v0.28.3; \ - fi meson setup --default-library=shared $(BUILDDIR) $(SHARED_OPTS) meson configure --default-library=shared $(BUILDDIR) $(SHARED_OPTS) .PHONY: dev-rust -dev-rust: +dev-rust: .build/bin/uniffi-bindgen-go-forked cargo build EXT=so; \ if [ "$(BUILDOS)" = "darwin" ]; then EXT=dylib; fi; \ - uniffi-bindgen-go --out-dir pkg/iroh/generated --library ./target/debug/libiroh_streamplace.$$EXT \ + .build/bin/uniffi-bindgen-go-forked --out-dir pkg/iroh/generated --library ./target/debug/libiroh_streamplace.$$EXT \ && mkdir -p $(BUILDDIR)/rust/iroh-streamplace/ \ && mkdir -p $(BUILDDIR)/lib/ \ && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/rust/iroh-streamplace/ \ @@ -494,6 +491,11 @@ ci-npm-release: install echo //registry.npmjs.org/:_authToken=$$NPM_TOKEN > ~/.npmrc \ && npx lerna publish from-package --yes +.build/bin/uniffi-bindgen-go-forked: + mkdir -p .build \ + && cargo install uniffi-bindgen-go --git https://github.com/kegsay/uniffi-bindgen-go --rev f1f1064871faf05377c75e098d525d530d402d38 --root .build \ + && mv .build/bin/uniffi-bindgen-go .build/bin/uniffi-bindgen-go-forked + .build/bundletool.jar: mkdir -p .build \ && curl -L -o ./.build/bundletool.jar https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar -- 2.51.2 From 8d33e4f6129af71ccf3c5d959e347f78b6eb609a Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 30 Sep 2025 17:29:12 -0700 Subject: [PATCH 082/525] v0.7.34 --- js/app/package.json | 2 +- js/components/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index 72802fe6..21eb370c 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.33", + "version": "0.7.34", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/components/package.json b/js/components/package.json index 43cdf54d..eb9cb690 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@streamplace/components", - "version": "0.7.32", + "version": "0.7.34", "description": "Streamplace React (Native) Components", "main": "dist/index.js", "types": "src/index.tsx", diff --git a/js/docs/package.json b/js/docs/package.json index f1426974..b4c12ebf 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.33", + "version": "0.7.34", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index d8ef0959..3cb5a03c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.33", + "version": "0.7.34", "npmClient": "pnpm" } -- 2.51.2 From 03c74c973aa7647037bdf88c4c44dc9725d5e9da Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 1 Oct 2025 14:33:23 -0500 Subject: [PATCH 083/525] add problems back in --- .../components/live-dashboard/bento-grid.tsx | 199 +++++++++--------- .../src/components/dashboard/problems.tsx | 80 +++---- 2 files changed, 145 insertions(+), 134 deletions(-) diff --git a/js/app/components/live-dashboard/bento-grid.tsx b/js/app/components/live-dashboard/bento-grid.tsx index a9d456e9..3e512dc5 100644 --- a/js/app/components/live-dashboard/bento-grid.tsx +++ b/js/app/components/live-dashboard/bento-grid.tsx @@ -9,6 +9,7 @@ import { useSegmentTiming, zero, } from "@streamplace/components"; +import { ProblemsWrapper } from "@streamplace/components/src/components/dashboard/problems"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Dimensions, Platform, ScrollView, View } from "react-native"; import LivestreamPanel from "./livestream-panel"; @@ -130,122 +131,130 @@ export default function BentoGrid({ if (isDesktop) { // Desktop layout (>= 1200px) - Original bento grid return ( - - - - - - + + + + + + + + + + + + + + + + - - - - + + - - - - - - - - + ); } return ( - - {/* Header always at top */} - - - - - {/* Fixed layout with flex */} - - {/* Stream Monitor Panel */} - - + + {/* Header always at top */} + + - {/* Chat Panel - takes remaining space */} - - - + {/* Fixed layout with flex */} + + {/* Stream Monitor Panel */} + + + - {/* Livestream Panel */} - - + {/* Chat Panel - takes remaining space */} + + + + + {/* Livestream Panel */} + + + - - + + ); } diff --git a/js/components/src/components/dashboard/problems.tsx b/js/components/src/components/dashboard/problems.tsx index 2843a916..284686a5 100644 --- a/js/components/src/components/dashboard/problems.tsx +++ b/js/components/src/components/dashboard/problems.tsx @@ -1,12 +1,32 @@ -import { ExternalLink } from "lucide-react-native"; +import { + CircleAlert, + CircleX, + ExternalLink, + Info, + Sparkle, +} from "lucide-react-native"; import { useState } from "react"; -import { Linking, Pressable, Text, View } from "react-native"; +import { Linking, Pressable, View } from "react-native"; import { useLivestreamStore } from "../../livestream-store"; import { LivestreamProblem } from "../../livestream-store/livestream-state"; import * as zero from "../../ui"; +import { Button, Text } from "../ui"; const { bg, r, borders, p, text, layout, gap } = zero; +const getIcon = (severity: string) => { + switch (severity) { + case "error": + return ; + case "warning": + return ; + case "info": + return ; + default: + return ; + } +}; + const Problems = ({ probs, onIgnore, @@ -15,9 +35,9 @@ const Problems = ({ onIgnore: () => void; }) => { return ( - - - + + + Optimize Your Stream @@ -34,26 +54,22 @@ const Problems = ({ { gap: 8, alignItems: "flex-start" }, ]} > - - {p.severity} - + {getIcon(p.severity)} + {p.code} @@ -79,19 +95,11 @@ const Problems = ({ ))} - - - Ignore - + + + ); }; @@ -105,13 +113,7 @@ export const ProblemsWrapper = ({ const [dismiss, setDismiss] = useState(false); return ( - + <> {children} {problems.length > 0 && !dismiss && ( @@ -144,7 +146,7 @@ export const ProblemsWrapper = ({ )} - + ); }; -- 2.51.2 From 00acf64d4dc742f8c70556b1935793f7c73a0235 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 1 Oct 2025 15:39:43 -0500 Subject: [PATCH 084/525] add web link behavior in sidebar --- js/app/components/aqlink.tsx | 19 ++++++++++++ js/app/components/sidebar/sidebar-item.tsx | 34 +++++++++++++++------- js/app/components/sidebar/sidebar.tsx | 34 ++++++++++++---------- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/js/app/components/aqlink.tsx b/js/app/components/aqlink.tsx index 168affb4..f40f510c 100644 --- a/js/app/components/aqlink.tsx +++ b/js/app/components/aqlink.tsx @@ -2,6 +2,7 @@ import { Link, NavigationProp, ParamListBase, + useLinkBuilder, useNavigation, } from "@react-navigation/native"; import usePlatform from "hooks/usePlatform"; @@ -54,3 +55,21 @@ export function Redirect({ to }: { to: LinkParams }) { }, []); return ; } + +// generates the proper href for a given LinkParams object, for better web support +export function useAQLinkHref(to: LinkParams): { href?: string } { + const { isWeb } = usePlatform(); + const buildLink = useLinkBuilder(); + + if (!isWeb) { + return { href: undefined }; + } + + try { + const href = buildLink(to.screen, to.params); + return { href }; + } catch (e) { + console.warn("Failed to build link for", to, e); + return { href: undefined }; + } +} diff --git a/js/app/components/sidebar/sidebar-item.tsx b/js/app/components/sidebar/sidebar-item.tsx index 779fa77e..9748bc13 100644 --- a/js/app/components/sidebar/sidebar-item.tsx +++ b/js/app/components/sidebar/sidebar-item.tsx @@ -1,12 +1,17 @@ -import { Text, useTheme } from "@streamplace/components"; +import { DrawerNavigationState, ParamListBase } from "@react-navigation/native"; +import { Text, useTheme, zero } from "@streamplace/components"; +import { useAQLinkHref } from "components/aqlink"; import React, { ReactNode, useState } from "react"; import { + GestureResponderEvent, + Pressable, PressableStateCallbackType, StyleProp, View, ViewStyle, } from "react-native"; -import { Pressable } from "react-native-gesture-handler"; + +import {} from "src/router"; export default function SidebarItem({ icon, @@ -14,6 +19,7 @@ export default function SidebarItem({ collapsed, active, onPress, + route, style = null, tint = "rgba(189, 110, 134)", }: { @@ -24,7 +30,8 @@ export default function SidebarItem({ label: string | ReactNode; collapsed: boolean; active: boolean; - onPress: () => void; + onPress: (event: GestureResponderEvent) => void; + route?: DrawerNavigationState["routes"][number]; style?: | StyleProp | ((state: PressableStateCallbackType) => StyleProp); @@ -32,6 +39,10 @@ export default function SidebarItem({ }) { const [hover, setHover] = useState(false); const theme = useTheme(); + const { href } = useAQLinkHref({ + screen: route?.name || "Home", + params: route?.params as any, + }); // Handle different icon types - component, JSX element, or function returning JSX const renderIcon = () => { @@ -73,9 +84,18 @@ export default function SidebarItem({ style={style} onHoverIn={() => setHover(true)} onHoverOut={() => setHover(false)} + role="link" + accessibilityLabel={typeof label === "string" ? label : "Sidebar Item"} + // @ts-ignore This makes it render as on web! + href={href} > - {renderIcon()} + {renderIcon()} {!collapsed && ( { return { minWidth: widthAnim.value, @@ -61,26 +59,29 @@ export default function Sidebar({ return ( {!collapsed && Streamplace} @@ -98,6 +99,7 @@ export default function Sidebar({ | React.ComponentType | undefined; + console.log("router", route); return ( { + route={route} + onPress={(ev) => { + ev.preventDefault(); if (route.name === "Home") { - // copy logic for 'Home' to reset the stack + // reset the stack (b/c streamlist is in the same stack as home) navigation.dispatch( CommonActions.reset({ index: 0, @@ -126,7 +130,7 @@ export default function Sidebar({ } }} style={options.drawerItemStyle} - tint={options.drawerActiveTintColor as string} // Assuming tint is a string color or undefined + tint={options.drawerActiveTintColor as string} /> ); })} -- 2.51.2 From bf2fd1af1888ef09528ce421b4073b3941f2b81c Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 1 Oct 2025 15:42:28 -0500 Subject: [PATCH 085/525] clear up console.logs --- js/app/components/sidebar/sidebar-item.tsx | 8 +++++++- js/app/components/sidebar/sidebar.tsx | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/js/app/components/sidebar/sidebar-item.tsx b/js/app/components/sidebar/sidebar-item.tsx index 9748bc13..743baac3 100644 --- a/js/app/components/sidebar/sidebar-item.tsx +++ b/js/app/components/sidebar/sidebar-item.tsx @@ -73,7 +73,13 @@ export default function SidebarItem({ } // Fallback - console.log("tried to render item, but couldn't", (icon as any).$$typeof); + console.log( + "tried to render item for route", + label, + href, + ", but couldn't", + (icon as any).$$typeof, + ); return 📄; }; diff --git a/js/app/components/sidebar/sidebar.tsx b/js/app/components/sidebar/sidebar.tsx index 06b55a97..feb04455 100644 --- a/js/app/components/sidebar/sidebar.tsx +++ b/js/app/components/sidebar/sidebar.tsx @@ -99,7 +99,6 @@ export default function Sidebar({ | React.ComponentType | undefined; - console.log("router", route); return ( Date: Wed, 1 Oct 2025 15:48:11 -0500 Subject: [PATCH 086/525] skip rendering "display: none" components --- js/app/components/sidebar/sidebar.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/js/app/components/sidebar/sidebar.tsx b/js/app/components/sidebar/sidebar.tsx index feb04455..8139342c 100644 --- a/js/app/components/sidebar/sidebar.tsx +++ b/js/app/components/sidebar/sidebar.tsx @@ -99,6 +99,21 @@ export default function Sidebar({ | React.ComponentType | undefined; + // if we have style display: none on the drawer item, completely skip rendering it + const drawerItemStyle = options.drawerItemStyle; + let isHidden = false; + if ( + drawerItemStyle && + typeof drawerItemStyle === "object" && + "display" in drawerItemStyle && + (drawerItemStyle as any).display === "none" + ) { + isHidden = true; + } + if (isHidden) { + return null; + } + return ( Date: Wed, 1 Oct 2025 15:50:20 -0500 Subject: [PATCH 087/525] better accessibility label defaults? --- js/app/components/sidebar/sidebar-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/app/components/sidebar/sidebar-item.tsx b/js/app/components/sidebar/sidebar-item.tsx index 743baac3..1dab7ce5 100644 --- a/js/app/components/sidebar/sidebar-item.tsx +++ b/js/app/components/sidebar/sidebar-item.tsx @@ -91,7 +91,7 @@ export default function SidebarItem({ onHoverIn={() => setHover(true)} onHoverOut={() => setHover(false)} role="link" - accessibilityLabel={typeof label === "string" ? label : "Sidebar Item"} + accessibilityLabel={typeof label === "string" ? label : "Link to " + href} // @ts-ignore This makes it render as on web! href={href} > -- 2.51.2 From 8545aa2313be829af6edf42fd15e78e6c08d9691 Mon Sep 17 00:00:00 2001 From: natalie <22222885+espeon@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:54:21 -0500 Subject: [PATCH 088/525] Update js/components/src/streamplace-store/graph.tsx Co-authored-by: Eli Mallon --- js/components/src/streamplace-store/graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/src/streamplace-store/graph.tsx b/js/components/src/streamplace-store/graph.tsx index 288fced1..ee1cb1b3 100644 --- a/js/components/src/streamplace-store/graph.tsx +++ b/js/components/src/streamplace-store/graph.tsx @@ -125,7 +125,7 @@ export function useGraphManager( } } catch (err) { setError( - `Could not determine follow state: ${err instanceof Error ? err.message : "Unknown error"}`, + `Could not determine follow state: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`, ); setIsFollowing(null); } finally { -- 2.51.2 From fc5a15b5ef82848205a1c22275156e3ca03a78ec Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 25 Sep 2025 20:41:56 -0500 Subject: [PATCH 089/525] initial rotation implementation --- js/app/app.config.ts | 1 + js/app/components/mobile/player.tsx | 63 +++---- js/app/components/mobile/ui.tsx | 31 +++- js/components/package.json | 2 + .../mobile-player/rotation-lock.tsx | 162 ++++++++++++++++++ js/components/src/index.tsx | 10 ++ 6 files changed, 232 insertions(+), 37 deletions(-) create mode 100644 js/components/src/components/mobile-player/rotation-lock.tsx diff --git a/js/app/app.config.ts b/js/app/app.config.ts index de6c2f33..7e275cfd 100644 --- a/js/app/app.config.ts +++ b/js/app/app.config.ts @@ -197,6 +197,7 @@ export default function () { withAndroidProfileable, "expo-video", "expo-web-browser", + "expo-screen-orientation", streamplaceReactNativeWebRTC, [ "expo-video", diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index e23b8967..47bd37b3 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -7,6 +7,7 @@ import { PlayerProps, PlayerProvider, PlayerUI, + RotationProvider, Text, usePlayerDimensions, usePlayerStore, @@ -19,7 +20,7 @@ import { useLiveUser } from "hooks/useLiveUser"; import { useSidebarControl } from "hooks/useSidebarControl"; import { ArrowLeft, ArrowRight } from "lucide-react-native"; import { ComponentRef, useEffect, useRef, useState } from "react"; -import { Animated, ScrollView, StatusBar } from "react-native"; +import { Animated, Platform, ScrollView, StatusBar } from "react-native"; import { useAppSelector } from "store/hooks"; import { BottomMetadata } from "./bottom-metadata"; import { DesktopChatPanel } from "./chat"; @@ -121,36 +122,38 @@ export function Player( } return ( - - + ); } diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index f318e567..12e04fe2 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -10,6 +10,7 @@ import { useMuted, usePlayerDimensions, usePlayerStore, + useRotation, useSegmentDimensions, useSetMuted, useSetVolume, @@ -23,6 +24,7 @@ import { ChevronRight, Fullscreen, Minimize, + RotateCw, SwitchCamera, Volume2, VolumeX, @@ -375,6 +377,9 @@ function RightControlsPanel({ const setMuted = useSetMuted(); const fullscreen = usePlayerStore((x) => x.fullscreen); const setFullscreen = usePlayerStore((x) => x.setFullscreen); + const { toggleRotation, canRotate, currentOrientation } = useRotation(); + + console.log("rotation", canRotate, currentOrientation); const [showVolumeSlider, setShowVolumeSlider] = useState(false); @@ -422,23 +427,24 @@ function RightControlsPanel({ {ingest === null ? ( Platform.OS === "web" && ) : ( - - - + <> + + + + )} {Platform.OS === "web" ? ( <> { - console.log("UI.tsx Button pressed at:", Date.now()); setFullscreen(!fullscreen); }} style={[zero.p[2], r[1]]} > {fullscreen ? ( - + ) : ( - + )} @@ -450,7 +456,18 @@ function RightControlsPanel({ ) : ( - + <> + {canRotate && ( + { + toggleRotation(); + }} + > + + + )} + + )} {shouldShowChatSidePanel && setShowChat && ( Promise; + rotateToPortrait: () => Promise; + toggleRotation: () => Promise; + canRotate: boolean; +} + +export interface RotationProviderProps { + children: React.ReactNode; + enabled?: boolean; +} + +const RotationContext = createContext(null); + +export const RotationProvider: React.FC = ({ + children, + enabled = true, +}) => { + const [isLocked, setIsLocked] = useState(false); + const [canRotate, setCanRotate] = useState(true); + const currentOrientation = useRef(Orientation.PORTRAIT_UP); + + // Manual rotation functions + const rotateToLandscape = async () => { + if (!enabled || !canRotate) return; + + try { + await ScreenOrientation.unlockAsync(); + await ScreenOrientation.lockAsync(OrientationLock.LANDSCAPE_RIGHT); + setIsLocked(true); + + // set current orientation to landscape left for consistency + currentOrientation.current = Orientation.LANDSCAPE_RIGHT; + + if (__DEV__) { + console.log("📲 Manual landscape"); + } + } catch (error) { + console.warn("Failed to rotate to landscape:", error); + } + }; + + const rotateToPortrait = async () => { + if (!enabled || !canRotate) return; + + try { + await ScreenOrientation.lockAsync(OrientationLock.PORTRAIT_UP); + setIsLocked(true); + + currentOrientation.current = Orientation.PORTRAIT_UP; + + if (__DEV__) { + console.log("📲 Manual portrait"); + } + } catch (error) { + console.warn("Failed to rotate to portrait:", error); + } + }; + + const toggleRotation = async () => { + const isLandscape = + currentOrientation.current === Orientation.LANDSCAPE_LEFT || + currentOrientation.current === Orientation.LANDSCAPE_RIGHT; + + if (__DEV__) { + console.log( + `🔄 Toggle: current=${currentOrientation.current}, isLandscape=${isLandscape}`, + ); + } + + if (isLandscape) { + await rotateToPortrait(); + } else { + await rotateToLandscape(); + } + }; + + // Track orientation changes + useEffect(() => { + if (!enabled) return; + + const getCurrentOrientation = async () => { + try { + const orient = await ScreenOrientation.getOrientationAsync(); + currentOrientation.current = orient; + + if (__DEV__) { + console.log(`📲 Orientation on load: ${orient}`); + } + } catch (error) { + console.warn("Failed to get orientation:", error); + } + }; + + getCurrentOrientation(); + + const subscription = ScreenOrientation.addOrientationChangeListener( + (event) => { + const newOrientation = event.orientationInfo.orientation; + currentOrientation.current = newOrientation; + + if (__DEV__) { + console.log( + `🔄 Orientation: ${newOrientation} (locked: ${isLocked})`, + ); + } + + // Only allow natural rotation when unlocked + if (!isLocked) { + // Natural rotation is happening, let it continue + } + }, + ); + + return () => { + subscription?.remove(); + ScreenOrientation.removeOrientationChangeListeners(); + ScreenOrientation.unlockAsync().catch(() => {}); + }; + }, [enabled]); + + const contextValue = useMemo( + () => ({ + currentOrientation: currentOrientation.current, + isLocked, + isActive: enabled, + rotateToLandscape, + rotateToPortrait, + toggleRotation, + canRotate, + }), + [isLocked, enabled, canRotate], + ); + + return ( + + {children} + + ); +}; + +export const useRotation = (): RotationContextValue => { + const context = useContext(RotationContext); + if (!context) { + throw new Error("useRotation must be used within a RotationProvider"); + } + return context; +}; diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx index 321e6dd6..1265f34e 100644 --- a/js/components/src/index.tsx +++ b/js/components/src/index.tsx @@ -33,6 +33,16 @@ export * from "./components/chat/system-message"; export { default as VideoRetry } from "./components/mobile-player/video-retry"; export * from "./lib/system-messages"; +// Rotation lock system exports +export { + RotationProvider, + useRotation, +} from "./components/mobile-player/rotation-lock"; +export type { + RotationContextValue, + RotationProviderProps, +} from "./components/mobile-player/rotation-lock"; + export * from "./components/share/sharesheet"; export * from "./components/keep-awake"; -- 2.51.2 From fdeab7a2a1c42cd0d3fb04f3b4cee37112ab09a8 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 25 Sep 2025 20:57:00 -0500 Subject: [PATCH 090/525] add minimize/maximize buttons instead of static rotatecw --- js/app/components/mobile/ui.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 12e04fe2..418a06de 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -23,8 +23,8 @@ import { ChevronLeft, ChevronRight, Fullscreen, + Maximize, Minimize, - RotateCw, SwitchCamera, Volume2, VolumeX, @@ -419,7 +419,7 @@ function RightControlsPanel({ : zero.layout.flex.row, zero.layout.flex.center, zero.gap.all[4], - zero.py[2], + zero.py[3], showChat === undefined ? zero.px[2] : zero.px[3], zero.layout.position.relative, ]} @@ -456,18 +456,30 @@ function RightControlsPanel({ ) : ( - <> + {canRotate && ( { toggleRotation(); }} > - + {currentOrientation === 1 ? ( + + ) : ( + + )} )} - + )} {shouldShowChatSidePanel && setShowChat && ( Date: Thu, 25 Sep 2025 21:40:25 -0500 Subject: [PATCH 091/525] hide sidebar in landscape mode --- js/app/components/mobile/player.tsx | 22 +++++++++++++++++++ .../components/mobile/useResponsiveLayout.ts | 8 ++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 47bd37b3..274a5299 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -29,6 +29,12 @@ import { OfflineCounter } from "./offline-counter"; import { MobileUi } from "./ui"; import { useResponsiveLayout } from "./useResponsiveLayout"; +import { + setSidebarHidden, + setSidebarUnhidden, +} from "features/base/sidebarSlice"; +import { useDispatch } from "react-redux"; + export function Player( props: Partial & { setFullscreen?: (fullscreen: boolean) => void; @@ -179,6 +185,9 @@ export function PlayerInner( showChatSidePanelOnLandscape: props.showChat, }); + // for hiding sidebar + const dispatch = useDispatch(); + // content info const { width, height } = usePlayerDimensions(); @@ -186,6 +195,19 @@ export function PlayerInner( // Calculate aspect ratio and determine if we're in desktop mode const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; + + // on mobile we want to hide the sidebar when going fullscreen + useEffect(() => { + if (Platform.OS !== "web" && width > height) { + console.log("hiding sb"); + dispatch(setSidebarHidden()); + } else { + dispatch(setSidebarUnhidden()); + } + return () => { + dispatch(setSidebarUnhidden()); + }; + }, [width, height]); // should cover full width on mobile? const isDesktopMode = shouldShowChatSidePanel || screenWidth > 1200; diff --git a/js/app/components/mobile/useResponsiveLayout.ts b/js/app/components/mobile/useResponsiveLayout.ts index 5d1b311c..81428b55 100644 --- a/js/app/components/mobile/useResponsiveLayout.ts +++ b/js/app/components/mobile/useResponsiveLayout.ts @@ -37,7 +37,13 @@ export function useResponsiveLayout({ return sidebarWidth.value; } return sidebarWidth; - }, [sidebarWidth]); + }, [ + sidebarWidth, + // weirddd + (sidebarWidth as any)?.value, + sidebarHidden, + showChatSidePanelOnLandscape, + ]); const isLandscape = screenWidth > screenHeight; const shouldShowChatSidePanel = -- 2.51.2 From 44688490cb543bba79eb3fcaea0ee5dddf4c751e Mon Sep 17 00:00:00 2001 From: natalie <22222885+espeon@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:16:13 -0500 Subject: [PATCH 092/525] Update js/app/components/mobile/ui.tsx Co-authored-by: Eli Mallon --- js/app/components/mobile/ui.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 418a06de..6ee6fa8d 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -379,7 +379,6 @@ function RightControlsPanel({ const setFullscreen = usePlayerStore((x) => x.setFullscreen); const { toggleRotation, canRotate, currentOrientation } = useRotation(); - console.log("rotation", canRotate, currentOrientation); const [showVolumeSlider, setShowVolumeSlider] = useState(false); -- 2.51.2 From 86070868f132fe5c21288439520f202768cb8aef Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 29 Sep 2025 17:24:35 -0500 Subject: [PATCH 093/525] try to native detection --- .../mobile-player/rotation-async.native.tsx | 15 ++++ .../mobile-player/rotation-lock.tsx | 68 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 js/components/src/components/mobile-player/rotation-async.native.tsx diff --git a/js/components/src/components/mobile-player/rotation-async.native.tsx b/js/components/src/components/mobile-player/rotation-async.native.tsx new file mode 100644 index 00000000..e3cff3ff --- /dev/null +++ b/js/components/src/components/mobile-player/rotation-async.native.tsx @@ -0,0 +1,15 @@ +let ScreenOrientation: typeof import("expo-screen-orientation") | null = null; + +try { + ScreenOrientation = require("expo-screen-orientation"); +} catch { + // expo-screen-orientation not available + if (__DEV__) { + console.warn( + "expo-screen-orientation not installed, rotation features disabled", + ); + } +} + +export const isRotationAvailable = ScreenOrientation != null; +export { ScreenOrientation }; diff --git a/js/components/src/components/mobile-player/rotation-lock.tsx b/js/components/src/components/mobile-player/rotation-lock.tsx index d641f3f6..00b0f9b9 100644 --- a/js/components/src/components/mobile-player/rotation-lock.tsx +++ b/js/components/src/components/mobile-player/rotation-lock.tsx @@ -1,5 +1,4 @@ -import * as ScreenOrientation from "expo-screen-orientation"; -import { Orientation, OrientationLock } from "expo-screen-orientation"; +import type { Orientation } from "expo-screen-orientation"; import React, { createContext, useContext, @@ -8,6 +7,10 @@ import React, { useRef, useState, } from "react"; +import { + isRotationAvailable, + ScreenOrientation, +} from "./rotation-async.native"; export interface RotationContextValue { currentOrientation: Orientation; @@ -31,20 +34,44 @@ export const RotationProvider: React.FC = ({ enabled = true, }) => { const [isLocked, setIsLocked] = useState(false); - const [canRotate, setCanRotate] = useState(true); - const currentOrientation = useRef(Orientation.PORTRAIT_UP); + const [canRotate, setCanRotate] = useState(isRotationAvailable); + const currentOrientation = useRef( + ScreenOrientation?.Orientation.PORTRAIT_UP ?? 1, + ); + + // If module not available, provide disabled context + if (!isRotationAvailable || !ScreenOrientation) { + const disabledContextValue: RotationContextValue = { + currentOrientation: 1, // Orientation.PORTRAIT_UP + isLocked: false, + isActive: false, + rotateToLandscape: async () => {}, + rotateToPortrait: async () => {}, + toggleRotation: async () => {}, + canRotate: false, + }; + + return ( + + {children} + + ); + } // Manual rotation functions const rotateToLandscape = async () => { - if (!enabled || !canRotate) return; + if (!enabled || !canRotate || !ScreenOrientation) return; try { await ScreenOrientation.unlockAsync(); - await ScreenOrientation.lockAsync(OrientationLock.LANDSCAPE_RIGHT); + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, + ); setIsLocked(true); // set current orientation to landscape left for consistency - currentOrientation.current = Orientation.LANDSCAPE_RIGHT; + currentOrientation.current = + ScreenOrientation.Orientation.LANDSCAPE_RIGHT; if (__DEV__) { console.log("📲 Manual landscape"); @@ -55,13 +82,15 @@ export const RotationProvider: React.FC = ({ }; const rotateToPortrait = async () => { - if (!enabled || !canRotate) return; + if (!enabled || !canRotate || !ScreenOrientation) return; try { - await ScreenOrientation.lockAsync(OrientationLock.PORTRAIT_UP); + await ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ); setIsLocked(true); - currentOrientation.current = Orientation.PORTRAIT_UP; + currentOrientation.current = ScreenOrientation.Orientation.PORTRAIT_UP; if (__DEV__) { console.log("📲 Manual portrait"); @@ -72,9 +101,13 @@ export const RotationProvider: React.FC = ({ }; const toggleRotation = async () => { + if (!ScreenOrientation) return; + const isLandscape = - currentOrientation.current === Orientation.LANDSCAPE_LEFT || - currentOrientation.current === Orientation.LANDSCAPE_RIGHT; + currentOrientation.current === + ScreenOrientation.Orientation.LANDSCAPE_LEFT || + currentOrientation.current === + ScreenOrientation.Orientation.LANDSCAPE_RIGHT; if (__DEV__) { console.log( @@ -94,6 +127,7 @@ export const RotationProvider: React.FC = ({ if (!enabled) return; const getCurrentOrientation = async () => { + if (!ScreenOrientation) return; try { const orient = await ScreenOrientation.getOrientationAsync(); currentOrientation.current = orient; @@ -108,6 +142,8 @@ export const RotationProvider: React.FC = ({ getCurrentOrientation(); + if (!ScreenOrientation) return; + const subscription = ScreenOrientation.addOrientationChangeListener( (event) => { const newOrientation = event.orientationInfo.orientation; @@ -128,10 +164,12 @@ export const RotationProvider: React.FC = ({ return () => { subscription?.remove(); - ScreenOrientation.removeOrientationChangeListeners(); - ScreenOrientation.unlockAsync().catch(() => {}); + if (ScreenOrientation) { + ScreenOrientation.removeOrientationChangeListeners(); + ScreenOrientation.unlockAsync().catch(() => {}); + } }; - }, [enabled]); + }, [enabled, isLocked]); const contextValue = useMemo( () => ({ -- 2.51.2 From 16bb1d0832d39aebb5888dd3801ebcf33f29e29e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 2 Oct 2025 12:36:11 -0500 Subject: [PATCH 094/525] refresh lockfile --- pnpm-lock.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27ed35cb..4182423a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -413,6 +413,12 @@ importers: expo-keep-awake: specifier: ^14.0.0 version: 14.1.4(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) + expo-screen-orientation: + specifier: ^9.0.7 + version: 9.0.7(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)) + expo-sensors: + specifier: ^15.0.7 + version: 15.0.7(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)) expo-sqlite: specifier: ~15.2.12 version: 15.2.12(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) @@ -6849,6 +6855,12 @@ packages: expo: '*' react-native: '*' + expo-sensors@15.0.7: + resolution: {integrity: sha512-TGUxRx/Ss7KGgfWo453YF64ENucw6oYryPiu/8I3ZZuf114xQPRxAbsZohPLaVUUGuaUyWbDsb0eRsmuKUzBnQ==} + peerDependencies: + expo: '*' + react-native: '*' + expo-splash-screen@0.30.9: resolution: {integrity: sha512-curHUaZxUTZ2dWvz32ao3xPv5mJr1LBqn5V8xm/IULAehB9RGCn8iKiROMN1PYebSG+56vPMuJmBm9P+ayvJpA==} peerDependencies: @@ -21230,6 +21242,17 @@ snapshots: expo: 53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10) react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) + expo-screen-orientation@9.0.7(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)): + dependencies: + expo: 53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10) + react-native: 0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) + + expo-sensors@15.0.7(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)): + dependencies: + expo: 53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10) + invariant: 2.2.4 + react-native: 0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) + expo-splash-screen@0.30.9(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10)): dependencies: '@expo/prebuild-config': 9.0.6 -- 2.51.2 From 3e9693dcd19a13bbd7abaeee1a44aeeee08cb38d Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Thu, 2 Oct 2025 15:50:00 -0700 Subject: [PATCH 095/525] iroh replicator: extremely rough PoC --- pkg/cmd/streamplace.go | 4 +- pkg/director/director.go | 23 +++++++++- pkg/replication/iroh_replicator/kv.go | 65 ++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 6501bd33..adbb7da4 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -32,8 +32,6 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/notifications" - "stream.place/streamplace/pkg/replication" - "stream.place/streamplace/pkg/replication/boring" "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/rtmps" v0 "stream.place/streamplace/pkg/schema/v0" @@ -307,7 +305,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { log.Log(ctx, "successfully initialized hardware signer", "address", addr) signer = hwsigner } - var rep replication.Replicator = &boring.BoringReplicator{Peers: cli.Peers} + // var rep replication.Replicator = &boring.BoringReplicator{Peers: cli.Peers} mod, err := model.MakeDB(cli.DataFilePath([]string{"index"})) if err != nil { diff --git a/pkg/director/director.go b/pkg/director/director.go index 3ecf05e8..e9e75499 100644 --- a/pkg/director/director.go +++ b/pkg/director/director.go @@ -2,6 +2,7 @@ package director import ( "context" + "encoding/json" "fmt" "sync" @@ -50,6 +51,11 @@ func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus * } func (d *Director) Start(ctx context.Context) error { + nodeId, err := d.swarm.Node.NodeId() + if err != nil { + return fmt.Errorf("failed to get node id: %w", err) + } + newSeg := d.mm.NewSegment() ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -91,9 +97,24 @@ func (d *Director) Start(ctx context.Context) error { } d.streamSessionsMu.Unlock() go func() { - err := d.swarm.Put(ctx, not.Segment.RepoDID, not.Segment.StartTime.Format(util.ISO8601)) + originInfo := iroh_replicator.OriginInfo{ + NodeID: nodeId.String(), + Time: not.Segment.StartTime.Format(util.ISO8601), + } + bs, err := json.Marshal(originInfo) + if err != nil { + log.Error(ctx, "could not marshal origin info", "error", err) + return + } + err = d.swarm.Put(ctx, not.Segment.RepoDID, bs) if err != nil { log.Error(ctx, "could not put segment to swarm", "error", err) + return + } + err = d.swarm.Node.SendSegment(not.Segment.RepoDID, not.Data) + if err != nil { + log.Error(ctx, "could not send segment to swarm", "error", err) + return } }() err := ss.NewSegment(ctx, not) diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go index 68e63713..e11d63bb 100644 --- a/pkg/replication/iroh_replicator/kv.go +++ b/pkg/replication/iroh_replicator/kv.go @@ -2,6 +2,7 @@ package iroh_replicator import ( "context" + "encoding/json" "fmt" "time" @@ -10,12 +11,25 @@ import ( ) type SwarmKV struct { - node *iroh_streamplace.Node - db *iroh_streamplace.Db + Node *iroh_streamplace.Node + DB *iroh_streamplace.Db w *iroh_streamplace.WriteScope } +// A message saying "hey I ingested node data at this time" +type OriginInfo struct { + NodeID string `json:"node_id"` + Time string `json:"time"` +} + +type DataHandler struct{} + +func (handler *DataHandler) HandleData(topic string, data []byte) { + log.Log(context.Background(), "HandleData", "topic", topic, "data", len(data)) +} + func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, error) { + handler := &DataHandler{} ctx = log.WithLogValues(ctx, "func", "StartKV") log.Log(ctx, "Starting with tickets", "tickets", tickets) @@ -25,7 +39,7 @@ func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, er MaxSendDuration: 1000_000_000, // 1s } log.Log(ctx, "Config created", "config", config) - node, err := iroh_streamplace.NodeSender(config) + node, err := iroh_streamplace.NodeReceiver(config, handler) if err != nil { return nil, fmt.Errorf("failed to create NodeSender: %w", err) } @@ -46,22 +60,31 @@ func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, er log.Log(ctx, "Ticket:", "ticket", ticket) swarm := SwarmKV{ - node: node, - db: db, + Node: node, + DB: db, w: w, } return &swarm, nil } +var activeSubs = make(map[string]bool) + func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { if len(tickets) > 0 { - err := swarm.node.JoinPeers(tickets) + err := swarm.Node.JoinPeers(tickets) if err != nil { return fmt.Errorf("failed to join peers: %w", err) } } - sub := swarm.db.Subscribe(iroh_streamplace.NewFilter()) + nodeId, err := swarm.Node.NodeId() + if err != nil { + return fmt.Errorf("failed to get node id: %w", err) + } + nodeIdStr := nodeId.String() + log.Log(ctx, "Node ID:", "node_id", nodeIdStr) + + sub := swarm.DB.Subscribe(iroh_streamplace.NewFilter()) for { if ctx.Err() != nil { return ctx.Err() @@ -80,6 +103,29 @@ func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { keyStr := string(item.Key) valueStr := string(item.Value) log.Log(ctx, "SubscribeItemEntry", "key", keyStr, "value", valueStr) + var info OriginInfo + err := json.Unmarshal(item.Value, &info) + if err != nil { + log.Error(ctx, "could not unmarshal origin info", "error", err) + continue + } + if !activeSubs[keyStr] { + if info.NodeID == nodeIdStr { + activeSubs[keyStr] = true + continue + } + pubKey, err := iroh_streamplace.PublicKeyFromString(info.NodeID) + if err != nil { + log.Error(ctx, "could not create public key", "error", err) + continue + } + activeSubs[keyStr] = true + err = swarm.Node.Subscribe(keyStr, pubKey) + if err != nil { + log.Error(ctx, "could not subscribe to key", "error", err) + continue + } + } case iroh_streamplace.SubscribeItemCurrentDone: log.Log(ctx, "SubscribeItemCurrentDone", "currentDone", item) @@ -91,9 +137,8 @@ func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { } } -func (swarm *SwarmKV) Put(ctx context.Context, key, value string) error { +func (swarm *SwarmKV) Put(ctx context.Context, key string, value []byte) error { // streamerBs := []byte(streamer) keyBs := []byte(key) - valueBs := []byte(value) - return swarm.w.Put(nil, keyBs, valueBs) + return swarm.w.Put(nil, keyBs, value) } -- 2.51.2 From e7b5eb5d8dbbaedd2b835870e2972d808f5caf7f Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Thu, 2 Oct 2025 16:18:44 -0700 Subject: [PATCH 096/525] fixup --- pkg/cmd/streamplace.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index adbb7da4..6501bd33 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -32,6 +32,8 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/notifications" + "stream.place/streamplace/pkg/replication" + "stream.place/streamplace/pkg/replication/boring" "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/rtmps" v0 "stream.place/streamplace/pkg/schema/v0" @@ -305,7 +307,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { log.Log(ctx, "successfully initialized hardware signer", "address", addr) signer = hwsigner } - // var rep replication.Replicator = &boring.BoringReplicator{Peers: cli.Peers} + var rep replication.Replicator = &boring.BoringReplicator{Peers: cli.Peers} mod, err := model.MakeDB(cli.DataFilePath([]string{"index"})) if err != nil { -- 2.51.2 From a54f3bf10ac3a8c574bdd382951d70555567fd3a Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 2 Oct 2025 19:41:05 -0500 Subject: [PATCH 097/525] remove weird import? --- js/app/components/sidebar/sidebar-item.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/app/components/sidebar/sidebar-item.tsx b/js/app/components/sidebar/sidebar-item.tsx index 1dab7ce5..9dea23b4 100644 --- a/js/app/components/sidebar/sidebar-item.tsx +++ b/js/app/components/sidebar/sidebar-item.tsx @@ -11,8 +11,6 @@ import { ViewStyle, } from "react-native"; -import {} from "src/router"; - export default function SidebarItem({ icon, label, -- 2.51.2 From 70135f49ce201189c4b057f49cfc4b9b50bc5dea Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 2 Oct 2025 20:02:05 -0500 Subject: [PATCH 098/525] remove extra space oops --- js/app/components/mobile/ui.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 6ee6fa8d..227c06eb 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -379,7 +379,6 @@ function RightControlsPanel({ const setFullscreen = usePlayerStore((x) => x.setFullscreen); const { toggleRotation, canRotate, currentOrientation } = useRotation(); - const [showVolumeSlider, setShowVolumeSlider] = useState(false); const handleVolumePress = () => { -- 2.51.2 From da11ac91f76b3c87553cc9841a04ab7648834725 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 15:23:55 -0500 Subject: [PATCH 099/525] Wire up problems and show # of problems on the dashboard --- .../components/live-dashboard/bento-grid.tsx | 28 ++++++++----- js/app/src/screens/live-dashboard.tsx | 6 +-- .../src/components/dashboard/header.tsx | 40 +++++++++++++++++-- .../src/components/dashboard/index.tsx | 2 +- .../src/components/dashboard/problems.tsx | 23 +++++++---- 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/js/app/components/live-dashboard/bento-grid.tsx b/js/app/components/live-dashboard/bento-grid.tsx index 3e512dc5..6b8e025c 100644 --- a/js/app/components/live-dashboard/bento-grid.tsx +++ b/js/app/components/live-dashboard/bento-grid.tsx @@ -9,8 +9,11 @@ import { useSegmentTiming, zero, } from "@streamplace/components"; -import { ProblemsWrapper } from "@streamplace/components/src/components/dashboard/problems"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + ProblemsWrapper, + ProblemsWrapperRef, +} from "@streamplace/components/src/components/dashboard/problems"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Dimensions, Platform, ScrollView, View } from "react-native"; import LivestreamPanel from "./livestream-panel"; import StreamMonitor from "./stream-monitor"; @@ -18,18 +21,18 @@ import StreamMonitor from "./stream-monitor"; const { flex, p, gap, layout, bg } = zero; interface BentoGridProps { - userProfile: any; isLive: boolean; videoRef: any; } -export default function BentoGrid({ - userProfile, - isLive, - videoRef, -}: BentoGridProps) { +export default function BentoGrid({ isLive, videoRef }: BentoGridProps) { const navigation = useNavigation(); const isWeb = Platform.OS === "web"; + const problemsRef = useRef(null); + + const handleProblemsPress = useCallback(() => { + problemsRef.current?.setDismiss(false); + }, []); // Screen width state for responsive design const [screenWidth, setScreenWidth] = useState( @@ -63,6 +66,7 @@ export default function BentoGrid({ const profile = useProfile(); const viewers = useLivestreamStore((x) => x.viewers); const chat = useLivestreamStore((x) => x.chat); + const problems = useLivestreamStore((x) => x.problems); const segmentTiming = useSegmentTiming(); const seg = useSegment(); const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState); @@ -131,7 +135,7 @@ export default function BentoGrid({ if (isDesktop) { // Desktop layout (>= 1200px) - Original bento grid return ( - + @@ -201,7 +207,7 @@ export default function BentoGrid({ } return ( - + {/* Header always at top */} @@ -215,6 +221,8 @@ export default function BentoGrid({ bitrate={getBitrate()} timeBetweenSegments={segmentTiming.timeBetweenSegments || 0} connectionStatus={getConnectionStatus} + problemsCount={problems.length} + onProblemsPress={handleProblemsPress} /> diff --git a/js/app/src/screens/live-dashboard.tsx b/js/app/src/screens/live-dashboard.tsx index 373d659b..84db86bb 100644 --- a/js/app/src/screens/live-dashboard.tsx +++ b/js/app/src/screens/live-dashboard.tsx @@ -45,11 +45,7 @@ export default function LiveDashboard() { - + diff --git a/js/components/src/components/dashboard/header.tsx b/js/components/src/components/dashboard/header.tsx index 4a25882d..68758586 100644 --- a/js/components/src/components/dashboard/header.tsx +++ b/js/components/src/components/dashboard/header.tsx @@ -1,5 +1,5 @@ -import { Car, Radio, Users } from "lucide-react-native"; -import { Text, View } from "react-native"; +import { AlertCircle, Car, Radio, Users } from "lucide-react-native"; +import { Pressable, Text, View } from "react-native"; import * as zero from "../../ui"; const { bg, r, borders, px, py, text, layout, gap } = zero; @@ -103,6 +103,8 @@ interface HeaderProps { bitrate?: string; timeBetweenSegments?: number; connectionStatus?: "excellent" | "good" | "poor" | "offline"; + problemsCount?: number; + onProblemsPress?: () => void; } export default function Header({ @@ -113,6 +115,8 @@ export default function Header({ bitrate = "0 mbps", timeBetweenSegments = 0, connectionStatus = "offline", + problemsCount = 0, + onProblemsPress, }: HeaderProps) { const getConnectionQuality = (): "good" | "warning" | "error" => { if (timeBetweenSegments <= 1500) return "good"; @@ -139,7 +143,37 @@ export default function Header({ {streamTitle} - + + + {problemsCount > 0 && ( + + + + + {problemsCount} {problemsCount === 1 ? "Issue" : "Issues"} + + + + )} + diff --git a/js/components/src/components/dashboard/index.tsx b/js/components/src/components/dashboard/index.tsx index 49c1f920..81497d63 100644 --- a/js/components/src/components/dashboard/index.tsx +++ b/js/components/src/components/dashboard/index.tsx @@ -2,4 +2,4 @@ export { default as ChatPanel } from "./chat-panel"; export { default as Header } from "./header"; export { default as InformationWidget } from "./information-widget"; export { default as ModActions } from "./mod-actions"; -export { default as Problems } from "./problems"; +export { default as Problems, ProblemsWrapperRef } from "./problems"; diff --git a/js/components/src/components/dashboard/problems.tsx b/js/components/src/components/dashboard/problems.tsx index 284686a5..76309ddc 100644 --- a/js/components/src/components/dashboard/problems.tsx +++ b/js/components/src/components/dashboard/problems.tsx @@ -5,7 +5,7 @@ import { Info, Sparkle, } from "lucide-react-native"; -import { useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { Linking, Pressable, View } from "react-native"; import { useLivestreamStore } from "../../livestream-store"; import { LivestreamProblem } from "../../livestream-store/livestream-state"; @@ -104,14 +104,23 @@ const Problems = ({ ); }; -export const ProblemsWrapper = ({ - children, -}: { - children: React.ReactElement; -}) => { +export interface ProblemsWrapperRef { + setDismiss: (value: boolean) => void; +} + +export const ProblemsWrapper = forwardRef< + ProblemsWrapperRef, + { + children: React.ReactElement; + } +>(({ children }, ref) => { const problems = useLivestreamStore((x) => x.problems); const [dismiss, setDismiss] = useState(false); + useImperativeHandle(ref, () => ({ + setDismiss, + })); + return ( <> {children} @@ -148,6 +157,6 @@ export const ProblemsWrapper = ({ )} ); -}; +}); export default Problems; -- 2.51.2 From 62c571557882a86a61587eb07cdd9aa8b23ab163 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 16:40:46 -0500 Subject: [PATCH 100/525] Add mute words to discord webhook manager --- .../components/settings/webhook-manager.tsx | 34 ++++++++++++++++ .../content/docs/lex-reference/openapi.json | 24 ++++++++++++ .../place-stream-server-createwebhook.md | 29 +++++++++----- .../server/place-stream-server-defs.md | 39 ++++++++++++------- .../place-stream-server-updatewebhook.md | 31 +++++++++------ .../place/stream/server/createWebhook.json | 8 ++++ lexicons/place/stream/server/defs.json | 8 ++++ .../place/stream/server/updateWebhook.json | 8 ++++ pkg/integrations/webhook/manager.go | 14 +++++++ pkg/spxrpc/webhook.go | 7 ++++ pkg/statedb/webhook.go | 22 +++++++++++ pkg/streamplace/servercreateWebhook.go | 2 + pkg/streamplace/serverdefs.go | 2 + pkg/streamplace/serverupdateWebhook.go | 2 + 14 files changed, 194 insertions(+), 36 deletions(-) diff --git a/js/app/components/settings/webhook-manager.tsx b/js/app/components/settings/webhook-manager.tsx index 890163b9..baf7f704 100644 --- a/js/app/components/settings/webhook-manager.tsx +++ b/js/app/components/settings/webhook-manager.tsx @@ -50,6 +50,7 @@ interface Webhook { prefix?: string; suffix?: string; rewrite?: Array<{ from: string; to: string }>; + muteWords?: string[]; description?: string; createdAt: string; updatedAt?: string; @@ -65,6 +66,7 @@ interface WebhookFormData { prefix: string; suffix: string; rewrite: Array<{ from: string; to: string }>; + muteWords: string[]; description: string; } @@ -247,6 +249,7 @@ function WebhookForm({ prefix: webhook?.prefix || "", suffix: webhook?.suffix || "", rewrite: webhook?.rewrite || [{ from: "", to: "" }], + muteWords: webhook?.muteWords || [], description: webhook?.description || "", }); @@ -263,6 +266,7 @@ function WebhookForm({ prefix: webhook.prefix || "", suffix: webhook.suffix || "", rewrite: webhook.rewrite || [{ from: "", to: "" }], + muteWords: webhook.muteWords || [], description: webhook.description || "", }); } else { @@ -275,6 +279,7 @@ function WebhookForm({ prefix: "", suffix: "", rewrite: [{ from: "", to: "" }], + muteWords: [], description: "", }); } @@ -546,6 +551,33 @@ function WebhookForm({ ))} + {/* Mute Words */} + + + Mute Words (Chat Only) + + + Chat messages containing any of these words will not be forwarded. + Useful for avoiding reforwarding of forwarded messages. + + + setFormData((prev) => ({ + ...prev, + muteWords: text + .split(",") + .map((w) => w.trim()) + .filter((w) => w), + })) + } + placeholder="word1, word2, word3" + multiline + /> + + {/* Example message text */} 0 ? rewriteRules : undefined, + muteWords: data.muteWords.length > 0 ? data.muteWords : undefined, description: data.description || undefined, }); setShowForm(false); @@ -699,6 +732,7 @@ export default function WebhookManager() { prefix: data.prefix || undefined, suffix: data.suffix || undefined, rewrite: rewriteRules.length > 0 ? rewriteRules : undefined, + muteWords: data.muteWords.length > 0 ? data.muteWords : undefined, description: data.description || undefined, }); setShowForm(false); diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index a443368f..eac1b86c 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -110,6 +110,14 @@ "type": "string", "description": "A description of what this webhook is used for.", "maxLength": 500 + }, + "muteWords": { + "type": "array", + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded.", + "items": { + "type": "string", + "maxLength": 100 + } } }, "required": ["url", "events"] @@ -465,6 +473,14 @@ "type": "string", "description": "A description of what this webhook is used for.", "maxLength": 500 + }, + "muteWords": { + "type": "array", + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded.", + "items": { + "type": "string", + "maxLength": 100 + } } }, "required": ["id"] @@ -1655,6 +1671,14 @@ "errorCount": { "type": "integer", "description": "Number of consecutive errors for this webhook." + }, + "muteWords": { + "type": "array", + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded.", + "items": { + "type": "string", + "maxLength": 100 + } } }, "required": ["id", "url", "events", "active", "createdAt"] diff --git a/js/docs/src/content/docs/lex-reference/server/place-stream-server-createwebhook.md b/js/docs/src/content/docs/lex-reference/server/place-stream-server-createwebhook.md index 0a3f3205..60acae40 100644 --- a/js/docs/src/content/docs/lex-reference/server/place-stream-server-createwebhook.md +++ b/js/docs/src/content/docs/lex-reference/server/place-stream-server-createwebhook.md @@ -24,16 +24,17 @@ Create a new webhook for receiving Streamplace events. **Schema Type:** `object` -| Name | Type | Req'd | Description | Constraints | -| ------------- | ------------------------------------------------------------------------------------------------------ | ----- | ---------------------------------------------------- | ---------------- | -| `url` | `string` | ✅ | The webhook URL where events will be sent. | Format: `uri` | -| `events` | Array of `string` | ✅ | The types of events this webhook should receive. | | -| `active` | `boolean` | ❌ | Whether this webhook should be active upon creation. | Default: `false` | -| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | -| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | -| `rewrite` | Array of [`place.stream.server.defs#rewriteRule`](/lex-reference/place-stream-server-defs#rewriterule) | ❌ | Text replacement rules for webhook messages. | | -| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | -| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | +| Name | Type | Req'd | Description | Constraints | +| ------------- | ------------------------------------------------------------------------------------------------------ | ----- | ----------------------------------------------------------------------------------------------------- | ---------------- | +| `url` | `string` | ✅ | The webhook URL where events will be sent. | Format: `uri` | +| `events` | Array of `string` | ✅ | The types of events this webhook should receive. | | +| `active` | `boolean` | ❌ | Whether this webhook should be active upon creation. | Default: `false` | +| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | +| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | +| `rewrite` | Array of [`place.stream.server.defs#rewriteRule`](/lex-reference/place-stream-server-defs#rewriterule) | ❌ | Text replacement rules for webhook messages. | | +| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | +| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | +| `muteWords` | Array of `string` | ❌ | Words to filter out from chat messages. Messages containing any of these words will not be forwarded. | | **Output:** @@ -115,6 +116,14 @@ Create a new webhook for receiving Streamplace events. "type": "string", "maxLength": 500, "description": "A description of what this webhook is used for." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } } diff --git a/js/docs/src/content/docs/lex-reference/server/place-stream-server-defs.md b/js/docs/src/content/docs/lex-reference/server/place-stream-server-defs.md index 50ced8b6..a5eeb476 100644 --- a/js/docs/src/content/docs/lex-reference/server/place-stream-server-defs.md +++ b/js/docs/src/content/docs/lex-reference/server/place-stream-server-defs.md @@ -17,21 +17,22 @@ A webhook configuration for receiving Streamplace events. **Properties:** -| Name | Type | Req'd | Description | Constraints | -| --------------- | --------------------------------------- | ----- | ------------------------------------------------ | ------------------ | -| `id` | `string` | ✅ | Unique identifier for this webhook. | | -| `url` | `string` | ✅ | The webhook URL where events will be sent. | Format: `uri` | -| `events` | Array of `string` | ✅ | The types of events this webhook should receive. | | -| `active` | `boolean` | ✅ | Whether this webhook is currently active. | | -| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | -| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | -| `rewrite` | Array of [`#rewriteRule`](#rewriterule) | ❌ | Text replacement rules for webhook messages. | | -| `createdAt` | `string` | ✅ | When this webhook was created. | Format: `datetime` | -| `updatedAt` | `string` | ❌ | When this webhook was last updated. | Format: `datetime` | -| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | -| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | -| `lastTriggered` | `string` | ❌ | When this webhook was last triggered. | Format: `datetime` | -| `errorCount` | `integer` | ❌ | Number of consecutive errors for this webhook. | | +| Name | Type | Req'd | Description | Constraints | +| --------------- | --------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------- | ------------------ | +| `id` | `string` | ✅ | Unique identifier for this webhook. | | +| `url` | `string` | ✅ | The webhook URL where events will be sent. | Format: `uri` | +| `events` | Array of `string` | ✅ | The types of events this webhook should receive. | | +| `active` | `boolean` | ✅ | Whether this webhook is currently active. | | +| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | +| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | +| `rewrite` | Array of [`#rewriteRule`](#rewriterule) | ❌ | Text replacement rules for webhook messages. | | +| `createdAt` | `string` | ✅ | When this webhook was created. | Format: `datetime` | +| `updatedAt` | `string` | ❌ | When this webhook was last updated. | Format: `datetime` | +| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | +| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | +| `lastTriggered` | `string` | ❌ | When this webhook was last triggered. | Format: `datetime` | +| `errorCount` | `integer` | ❌ | Number of consecutive errors for this webhook. | | +| `muteWords` | Array of `string` | ❌ | Words to filter out from chat messages. Messages containing any of these words will not be forwarded. | | --- @@ -129,6 +130,14 @@ A webhook configuration for receiving Streamplace events. "errorCount": { "type": "integer", "description": "Number of consecutive errors for this webhook." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } }, diff --git a/js/docs/src/content/docs/lex-reference/server/place-stream-server-updatewebhook.md b/js/docs/src/content/docs/lex-reference/server/place-stream-server-updatewebhook.md index 220db4d6..67f883fe 100644 --- a/js/docs/src/content/docs/lex-reference/server/place-stream-server-updatewebhook.md +++ b/js/docs/src/content/docs/lex-reference/server/place-stream-server-updatewebhook.md @@ -24,17 +24,18 @@ Update an existing webhook configuration. **Schema Type:** `object` -| Name | Type | Req'd | Description | Constraints | -| ------------- | ------------------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------ | --------------- | -| `id` | `string` | ✅ | The ID of the webhook to update. | | -| `url` | `string` | ❌ | The webhook URL where events will be sent. | Format: `uri` | -| `events` | Array of `string` | ❌ | The types of events this webhook should receive. | | -| `active` | `boolean` | ❌ | Whether this webhook should be active. | | -| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | -| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | -| `rewrite` | Array of [`place.stream.server.defs#rewriteRule`](/lex-reference/place-stream-server-defs#rewriterule) | ❌ | Text replacement rules for webhook messages. | | -| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | -| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | +| Name | Type | Req'd | Description | Constraints | +| ------------- | ------------------------------------------------------------------------------------------------------ | ----- | ----------------------------------------------------------------------------------------------------- | --------------- | +| `id` | `string` | ✅ | The ID of the webhook to update. | | +| `url` | `string` | ❌ | The webhook URL where events will be sent. | Format: `uri` | +| `events` | Array of `string` | ❌ | The types of events this webhook should receive. | | +| `active` | `boolean` | ❌ | Whether this webhook should be active. | | +| `prefix` | `string` | ❌ | Text to prepend to webhook messages. | Max Length: 100 | +| `suffix` | `string` | ❌ | Text to append to webhook messages. | Max Length: 100 | +| `rewrite` | Array of [`place.stream.server.defs#rewriteRule`](/lex-reference/place-stream-server-defs#rewriterule) | ❌ | Text replacement rules for webhook messages. | | +| `name` | `string` | ❌ | A user-friendly name for this webhook. | Max Length: 100 | +| `description` | `string` | ❌ | A description of what this webhook is used for. | Max Length: 500 | +| `muteWords` | Array of `string` | ❌ | Words to filter out from chat messages. Messages containing any of these words will not be forwarded. | | **Output:** @@ -120,6 +121,14 @@ Update an existing webhook configuration. "type": "string", "maxLength": 500, "description": "A description of what this webhook is used for." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } } diff --git a/lexicons/place/stream/server/createWebhook.json b/lexicons/place/stream/server/createWebhook.json index e741e7e8..146ed437 100644 --- a/lexicons/place/stream/server/createWebhook.json +++ b/lexicons/place/stream/server/createWebhook.json @@ -57,6 +57,14 @@ "type": "string", "maxLength": 500, "description": "A description of what this webhook is used for." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } } diff --git a/lexicons/place/stream/server/defs.json b/lexicons/place/stream/server/defs.json index 023c3e01..45930914 100644 --- a/lexicons/place/stream/server/defs.json +++ b/lexicons/place/stream/server/defs.json @@ -71,6 +71,14 @@ "errorCount": { "type": "integer", "description": "Number of consecutive errors for this webhook." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } }, diff --git a/lexicons/place/stream/server/updateWebhook.json b/lexicons/place/stream/server/updateWebhook.json index 5960a3d6..4e1ecc4f 100644 --- a/lexicons/place/stream/server/updateWebhook.json +++ b/lexicons/place/stream/server/updateWebhook.json @@ -60,6 +60,14 @@ "type": "string", "maxLength": 500, "description": "A description of what this webhook is used for." + }, + "muteWords": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "description": "Words to filter out from chat messages. Messages containing any of these words will not be forwarded." } } } diff --git a/pkg/integrations/webhook/manager.go b/pkg/integrations/webhook/manager.go index 626046ca..9da62d79 100644 --- a/pkg/integrations/webhook/manager.go +++ b/pkg/integrations/webhook/manager.go @@ -3,6 +3,7 @@ package webhook import ( "context" "fmt" + "strings" "github.com/bluesky-social/indigo/api/bsky" "stream.place/streamplace/pkg/integrations/discord" @@ -12,6 +13,19 @@ import ( // SendChatWebhook sends chat message to a specific webhook func SendChatWebhook(ctx context.Context, webhook *streamplace.ServerDefs_Webhook, authorDID string, scm *streamplace.ChatDefs_MessageView) error { + // Check if message should be muted + if msg, ok := scm.Record.Val.(*streamplace.ChatMessage); ok { + if len(webhook.MuteWords) > 0 { + messageText := strings.ToLower(msg.Text) + for _, muteWord := range webhook.MuteWords { + if strings.Contains(messageText, strings.ToLower(muteWord)) { + // Message contains a mute word, skip forwarding + return nil + } + } + } + } + discordWebhook, err := webhookToDiscordWebhook(webhook) if err != nil { return fmt.Errorf("failed to convert webhook: %w", err) diff --git a/pkg/spxrpc/webhook.go b/pkg/spxrpc/webhook.go index fc5c2a28..7668d51b 100644 --- a/pkg/spxrpc/webhook.go +++ b/pkg/spxrpc/webhook.go @@ -214,6 +214,13 @@ func (s *Server) handlePlaceStreamServerUpdateWebhook(ctx context.Context, input if input.Description != nil { updates["description"] = *input.Description } + if input.MuteWords != nil { + muteWordsJSON, err := json.Marshal(input.MuteWords) + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "Invalid mute words format") + } + updates["mute_words"] = json.RawMessage(muteWordsJSON) + } if len(updates) == 0 { return nil, echo.NewHTTPError(http.StatusBadRequest, "No fields to update") diff --git a/pkg/statedb/webhook.go b/pkg/statedb/webhook.go index 78e95d49..375a784a 100644 --- a/pkg/statedb/webhook.go +++ b/pkg/statedb/webhook.go @@ -22,6 +22,7 @@ type Webhook struct { Prefix string `gorm:"column:prefix"` Suffix string `gorm:"column:suffix"` Rewrite json.RawMessage `gorm:"column:rewrite;type:json"` + MuteWords json.RawMessage `gorm:"column:mute_words;type:json"` Name string `gorm:"column:name"` Description string `gorm:"column:description"` CreatedAt time.Time `gorm:"column:created_at"` @@ -172,6 +173,14 @@ func (w *Webhook) ToLexicon() (*streamplace.ServerDefs_Webhook, error) { } } + var muteWords []string + if len(w.MuteWords) > 0 { + err := json.Unmarshal(w.MuteWords, &muteWords) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal mute words: %w", err) + } + } + createdAt := w.CreatedAt.Format(time.RFC3339) webhook := &streamplace.ServerDefs_Webhook{ @@ -195,6 +204,9 @@ func (w *Webhook) ToLexicon() (*streamplace.ServerDefs_Webhook, error) { if w.Description != "" { webhook.Description = &w.Description } + if len(muteWords) > 0 { + webhook.MuteWords = muteWords + } if !w.UpdatedAt.IsZero() { updatedAt := w.UpdatedAt.Format(time.RFC3339) webhook.UpdatedAt = &updatedAt @@ -248,6 +260,15 @@ func WebhookFromLexiconInput(input *streamplace.ServerCreateWebhook_Input, userD rewriteJSON = json.RawMessage(jsonBytes) } + var muteWordsJSON json.RawMessage + if len(input.MuteWords) > 0 { + jsonBytes, err := json.Marshal(input.MuteWords) + if err != nil { + return nil, fmt.Errorf("failed to marshal mute words: %w", err) + } + muteWordsJSON = json.RawMessage(jsonBytes) + } + webhook := &Webhook{ ID: id, UserDID: userDID, @@ -257,6 +278,7 @@ func WebhookFromLexiconInput(input *streamplace.ServerCreateWebhook_Input, userD CreatedAt: time.Now(), UpdatedAt: time.Now(), Rewrite: rewriteJSON, + MuteWords: muteWordsJSON, } // if active is provided, use that value diff --git a/pkg/streamplace/servercreateWebhook.go b/pkg/streamplace/servercreateWebhook.go index 810e06ab..8576ff81 100644 --- a/pkg/streamplace/servercreateWebhook.go +++ b/pkg/streamplace/servercreateWebhook.go @@ -18,6 +18,8 @@ type ServerCreateWebhook_Input struct { Description *string `json:"description,omitempty" cborgen:"description,omitempty"` // events: The types of events this webhook should receive. Events []string `json:"events" cborgen:"events"` + // muteWords: Words to filter out from chat messages. Messages containing any of these words will not be forwarded. + MuteWords []string `json:"muteWords,omitempty" cborgen:"muteWords,omitempty"` // name: A user-friendly name for this webhook. Name *string `json:"name,omitempty" cborgen:"name,omitempty"` // prefix: Text to prepend to webhook messages. diff --git a/pkg/streamplace/serverdefs.go b/pkg/streamplace/serverdefs.go index bd87285b..9aa1a617 100644 --- a/pkg/streamplace/serverdefs.go +++ b/pkg/streamplace/serverdefs.go @@ -30,6 +30,8 @@ type ServerDefs_Webhook struct { Id string `json:"id" cborgen:"id"` // lastTriggered: When this webhook was last triggered. LastTriggered *string `json:"lastTriggered,omitempty" cborgen:"lastTriggered,omitempty"` + // muteWords: Words to filter out from chat messages. Messages containing any of these words will not be forwarded. + MuteWords []string `json:"muteWords,omitempty" cborgen:"muteWords,omitempty"` // name: A user-friendly name for this webhook. Name *string `json:"name,omitempty" cborgen:"name,omitempty"` // prefix: Text to prepend to webhook messages. diff --git a/pkg/streamplace/serverupdateWebhook.go b/pkg/streamplace/serverupdateWebhook.go index d0bd6e81..08bab165 100644 --- a/pkg/streamplace/serverupdateWebhook.go +++ b/pkg/streamplace/serverupdateWebhook.go @@ -20,6 +20,8 @@ type ServerUpdateWebhook_Input struct { Events []string `json:"events,omitempty" cborgen:"events,omitempty"` // id: The ID of the webhook to update. Id string `json:"id" cborgen:"id"` + // muteWords: Words to filter out from chat messages. Messages containing any of these words will not be forwarded. + MuteWords []string `json:"muteWords,omitempty" cborgen:"muteWords,omitempty"` // name: A user-friendly name for this webhook. Name *string `json:"name,omitempty" cborgen:"name,omitempty"` // prefix: Text to prepend to webhook messages. -- 2.51.2 From eaefc85434e81e296c7248f6e6965a2f31d50ff8 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 14:43:30 -0700 Subject: [PATCH 101/525] v0.7.35 --- js/app/package.json | 2 +- js/components/package.json | 2 +- js/docs/package.json | 2 +- lerna.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/app/package.json b/js/app/package.json index 21eb370c..594e0d85 100644 --- a/js/app/package.json +++ b/js/app/package.json @@ -1,7 +1,7 @@ { "name": "@streamplace/app", "main": "./src/entrypoint.tsx", - "version": "0.7.34", + "version": "0.7.35", "runtimeVersion": "0.7.2", "scripts": { "start": "npx expo start -c --port 38081", diff --git a/js/components/package.json b/js/components/package.json index eb9cb690..0ee34887 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -1,6 +1,6 @@ { "name": "@streamplace/components", - "version": "0.7.34", + "version": "0.7.35", "description": "Streamplace React (Native) Components", "main": "dist/index.js", "types": "src/index.tsx", diff --git a/js/docs/package.json b/js/docs/package.json index b4c12ebf..1aae223a 100644 --- a/js/docs/package.json +++ b/js/docs/package.json @@ -1,7 +1,7 @@ { "name": "streamplace-docs", "type": "module", - "version": "0.7.34", + "version": "0.7.35", "scripts": { "dev": "astro dev --host 0.0.0.0 --port 38082", "start": "astro dev --host 0.0.0.0 --port 38082", diff --git a/lerna.json b/lerna.json index 3cb5a03c..2bebafa3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.7.34", + "version": "0.7.35", "npmClient": "pnpm" } -- 2.51.2 From cfeb5c9c241989ffb71249acee42edbde796c283 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 15:49:10 -0700 Subject: [PATCH 102/525] iroh: working segment replication!! --- pkg/api/api.go | 2 +- pkg/cmd/streamplace.go | 9 +- pkg/config/config.go | 2 +- pkg/director/director.go | 34 +- .../iroh_streamplace/iroh_streamplace.go | 681 ------------------ .../iroh_streamplace/iroh_streamplace.h | 180 ----- pkg/iroh/iroh_streamplace_test.go | 56 -- pkg/media/media.go | 6 +- pkg/media/media_test.go | 5 +- pkg/media/rtcrec_test.go | 3 +- pkg/media/segmenter.go | 2 +- pkg/media/validate.go | 5 +- pkg/replication/iroh_replicator/iroh.go | 23 +- pkg/replication/iroh_replicator/kv.go | 112 ++- rust/iroh-streamplace/src/api.rs | 226 ------ rust/iroh-streamplace/src/endpoint.rs | 30 - rust/iroh-streamplace/src/lib.rs | 7 +- rust/iroh-streamplace/src/receiver.rs | 150 ---- rust/iroh-streamplace/src/sender.rs | 43 -- 19 files changed, 106 insertions(+), 1470 deletions(-) delete mode 100644 pkg/iroh/iroh_streamplace_test.go delete mode 100644 rust/iroh-streamplace/src/api.rs delete mode 100644 rust/iroh-streamplace/src/endpoint.rs delete mode 100644 rust/iroh-streamplace/src/receiver.rs delete mode 100644 rust/iroh-streamplace/src/sender.rs diff --git a/pkg/api/api.go b/pkg/api/api.go index d25bae5a..1653ddc3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -497,7 +497,7 @@ func (a *StreamplaceAPI) HandleNotification(ctx context.Context) http.HandlerFun func (a *StreamplaceAPI) HandleSegment(ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { - err := a.MediaManager.ValidateMP4(ctx, req.Body) + err := a.MediaManager.ValidateMP4(ctx, req.Body, false) if err != nil { apierrors.WriteHTTPBadRequest(w, "could not ingest segment", err) return diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 6501bd33..2c26758c 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -32,8 +32,6 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/notifications" - "stream.place/streamplace/pkg/replication" - "stream.place/streamplace/pkg/replication/boring" "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/rtmps" v0 "stream.place/streamplace/pkg/schema/v0" @@ -307,7 +305,6 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { log.Log(ctx, "successfully initialized hardware signer", "address", addr) signer = hwsigner } - var rep replication.Replicator = &boring.BoringReplicator{Peers: cli.Peers} mod, err := model.MakeDB(cli.DataFilePath([]string{"index"})) if err != nil { @@ -363,7 +360,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { return fmt.Errorf("failed to migrate: %w", err) } - mm, err := media.MakeMediaManager(ctx, &cli, signer, rep, mod, b, atsync) + mm, err := media.MakeMediaManager(ctx, &cli, signer, mod, b, atsync) if err != nil { return err } @@ -403,7 +400,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { return err } secret := buf.Bytes() - swarm, err := iroh_replicator.StartKV(ctx, cli.Tickets, secret) + swarm, err := iroh_replicator.NewSwarm(ctx, cli.Tickets, secret, mm) if err != nil { return err } @@ -419,7 +416,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { DownstreamJWK: cli.AccessJWK, ClientMetadata: clientMetadata, }) - d := director.NewDirector(mm, mod, &cli, b, op, state, swarm) + d := director.NewDirector(mm, mod, &cli, b, op, state) a, err := api.MakeStreamplaceAPI(&cli, mod, state, eip712signer, noter, mm, ms, b, atsync, d, op) if err != nil { return err diff --git a/pkg/config/config.go b/pkg/config/config.go index b71f54d3..f8743cbf 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -167,7 +167,7 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { fs.StringVar(&cli.PublicHost, "public-host", "", "public host for this streamplace node (excluding https:// e.g. stream.place)") fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation") fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps") - fs.BoolVar(&cli.ExternalSigning, "external-signing", true, "enable external signing via exec (prevents potential memory leak)") + fs.BoolVar(&cli.ExternalSigning, "external-signing", false, "enable external signing via exec (prevents potential memory leak)") fs.StringVar(&cli.TracingEndpoint, "tracing-endpoint", "", "gRPC endpoint to send traces to") fs.IntVar(&cli.RateLimitPerSecond, "rate-limit-per-second", 0, "rate limit for requests per second per ip") fs.IntVar(&cli.RateLimitBurst, "rate-limit-burst", 0, "rate limit burst for requests per ip") diff --git a/pkg/director/director.go b/pkg/director/director.go index e9e75499..d708bcb4 100644 --- a/pkg/director/director.go +++ b/pkg/director/director.go @@ -2,11 +2,9 @@ package director import ( "context" - "encoding/json" "fmt" "sync" - "github.com/bluesky-social/indigo/util" "github.com/streamplace/oatproxy/pkg/oatproxy" "golang.org/x/sync/errgroup" "stream.place/streamplace/pkg/bus" @@ -14,7 +12,6 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/model" - "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/statedb" ) @@ -33,10 +30,9 @@ type Director struct { streamSessionsMu sync.Mutex op *oatproxy.OATProxy statefulDB *statedb.StatefulDB - swarm *iroh_replicator.SwarmKV } -func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB, swarm *iroh_replicator.SwarmKV) *Director { +func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB) *Director { return &Director{ mm: mm, mod: mod, @@ -46,16 +42,10 @@ func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus * streamSessionsMu: sync.Mutex{}, op: op, statefulDB: statefulDB, - swarm: swarm, } } func (d *Director) Start(ctx context.Context) error { - nodeId, err := d.swarm.Node.NodeId() - if err != nil { - return fmt.Errorf("failed to get node id: %w", err) - } - newSeg := d.mm.NewSegment() ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -96,27 +86,7 @@ func (d *Director) Start(ctx context.Context) error { }) } d.streamSessionsMu.Unlock() - go func() { - originInfo := iroh_replicator.OriginInfo{ - NodeID: nodeId.String(), - Time: not.Segment.StartTime.Format(util.ISO8601), - } - bs, err := json.Marshal(originInfo) - if err != nil { - log.Error(ctx, "could not marshal origin info", "error", err) - return - } - err = d.swarm.Put(ctx, not.Segment.RepoDID, bs) - if err != nil { - log.Error(ctx, "could not put segment to swarm", "error", err) - return - } - err = d.swarm.Node.SendSegment(not.Segment.RepoDID, not.Data) - if err != nil { - log.Error(ctx, "could not send segment to swarm", "error", err) - return - } - }() + err := ss.NewSegment(ctx, not) if err != nil { log.Error(ctx, "could not add segment to stream session", "error", err) diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index e4125b08..d2b1feca 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -337,7 +337,6 @@ func readFloat64(reader io.Reader) float64 { func init() { FfiConverterDataHandlerINSTANCE.register() - FfiConverterDataHandlerOldINSTANCE.register() FfiConverterGoSignerINSTANCE.register() uniffiCheckChecksums() } @@ -389,15 +388,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_datahandler_handle_data: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data() - }) - if checksum != 20343 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_db_iter_with_opts() @@ -434,15 +424,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_db_write: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_endpoint_node_addr() - }) - if checksum != 17254 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_endpoint_node_addr: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_filter_global() @@ -650,51 +631,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_publickey_fmt_short: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_receiver_node_addr() - }) - if checksum != 10730 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_node_addr: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_receiver_subscribe() - }) - if checksum != 24145 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_subscribe: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe() - }) - if checksum != 21760 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_sender_node_addr() - }) - if checksum != 38541 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_sender_node_addr: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_method_sender_send() - }) - if checksum != 23930 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_sender_send: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_subscriberesponse_next_raw() @@ -713,15 +649,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_writescope_put: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_constructor_endpoint_new() - }) - if checksum != 60672 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_endpoint_new: UniFFI API checksum mismatch") - } - } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_constructor_filter_new() @@ -785,24 +712,6 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_publickey_from_string: UniFFI API checksum mismatch") } } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_constructor_receiver_new() - }) - if checksum != 18072 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_receiver_new: UniFFI API checksum mismatch") - } - } - { - checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { - return C.uniffi_iroh_streamplace_checksum_constructor_sender_new() - }) - if checksum != 56457 { - // If this happens try cleaning and rebuilding your project - panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_constructor_sender_new: UniFFI API checksum mismatch") - } - } } type FfiConverterUint64 struct{} @@ -1251,153 +1160,6 @@ func (c FfiConverterDataHandler) register() { C.uniffi_iroh_streamplace_fn_init_callback_vtable_datahandler(&UniffiVTableCallbackInterfaceDataHandlerINSTANCE) } -type DataHandlerOld interface { - HandleData(topic string, data []byte) -} -type DataHandlerOldImpl struct { - ffiObject FfiObject -} - -func (_self *DataHandlerOldImpl) HandleData(topic string, data []byte) { - _pointer := _self.ffiObject.incrementPointer("DataHandlerOld") - defer _self.ffiObject.decrementPointer() - uniffiRustCallAsync[error]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_datahandlerold_handle_data( - _pointer, FfiConverterStringINSTANCE.Lower(topic), FfiConverterBytesINSTANCE.Lower(data)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - -} -func (object *DataHandlerOldImpl) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterDataHandlerOld struct { - handleMap *concurrentHandleMap[DataHandlerOld] -} - -var FfiConverterDataHandlerOldINSTANCE = FfiConverterDataHandlerOld{ - handleMap: newConcurrentHandleMap[DataHandlerOld](), -} - -func (c FfiConverterDataHandlerOld) Lift(pointer unsafe.Pointer) DataHandlerOld { - result := &DataHandlerOldImpl{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_datahandlerold(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_datahandlerold(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*DataHandlerOldImpl).Destroy) - return result -} - -func (c FfiConverterDataHandlerOld) Read(reader io.Reader) DataHandlerOld { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterDataHandlerOld) Lower(value DataHandlerOld) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := unsafe.Pointer(uintptr(c.handleMap.insert(value))) - return pointer - -} - -func (c FfiConverterDataHandlerOld) Write(writer io.Writer, value DataHandlerOld) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerDataHandlerOld struct{} - -func (_ FfiDestroyerDataHandlerOld) Destroy(value DataHandlerOld) { - if val, ok := value.(*DataHandlerOldImpl); ok { - val.Destroy() - } else { - panic("Expected *DataHandlerOldImpl") - } -} - -//export iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0 -func iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0(uniffiHandle C.uint64_t, topic C.RustBuffer, data C.RustBuffer, uniffiFutureCallback C.UniffiForeignFutureCompleteVoid, uniffiCallbackData C.uint64_t, uniffiOutReturn *C.UniffiForeignFuture) { - handle := uint64(uniffiHandle) - uniffiObj, ok := FfiConverterDataHandlerOldINSTANCE.handleMap.tryGet(handle) - if !ok { - panic(fmt.Errorf("no callback in handle map: %d", handle)) - } - - result := make(chan C.UniffiForeignFutureStructVoid, 1) - cancel := make(chan struct{}, 1) - guardHandle := cgo.NewHandle(cancel) - *uniffiOutReturn = C.UniffiForeignFuture{ - handle: C.uint64_t(guardHandle), - free: C.UniffiForeignFutureFree(C.iroh_streamplace_uniffiFreeGorutine), - } - - // Wait for compleation or cancel - go func() { - select { - case <-cancel: - case res := <-result: - C.call_UniffiForeignFutureCompleteVoid(uniffiFutureCallback, uniffiCallbackData, res) - } - }() - - // Eval callback asynchroniously - go func() { - asyncResult := &C.UniffiForeignFutureStructVoid{} - defer func() { - result <- *asyncResult - }() - - uniffiObj.HandleData( - FfiConverterStringINSTANCE.Lift(GoRustBuffer{ - inner: topic, - }), - FfiConverterBytesINSTANCE.Lift(GoRustBuffer{ - inner: data, - }), - ) - - }() -} - -var UniffiVTableCallbackInterfaceDataHandlerOldINSTANCE = C.UniffiVTableCallbackInterfaceDataHandlerOld{ - handleData: (C.UniffiCallbackInterfaceDataHandlerOldMethod0)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0), - - uniffiFree: (C.UniffiCallbackInterfaceFree)(C.iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree), -} - -//export iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree -func iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree(handle C.uint64_t) { - FfiConverterDataHandlerOldINSTANCE.handleMap.remove(uint64(handle)) -} - -func (c FfiConverterDataHandlerOld) register() { - C.uniffi_iroh_streamplace_fn_init_callback_vtable_datahandlerold(&UniffiVTableCallbackInterfaceDataHandlerOldINSTANCE) -} - // Iroh-streamplace specific metadata database. type DbInterface interface { IterWithOpts(filter *Filter) ([]Entry, error) @@ -1529,121 +1291,6 @@ func (_ FfiDestroyerDb) Destroy(value *Db) { value.Destroy() } -type EndpointInterface interface { - NodeAddr() *NodeAddr -} -type Endpoint struct { - ffiObject FfiObject -} - -// Create a new endpoint. -func NewEndpoint() (*Endpoint, error) { - res, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *Endpoint { - return FfiConverterEndpointINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_constructor_endpoint_new(), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - if err == nil { - return res, nil - } - - return res, err -} - -func (_self *Endpoint) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Endpoint") - defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[error]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_method_endpoint_node_addr( - _pointer), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res -} -func (object *Endpoint) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterEndpoint struct{} - -var FfiConverterEndpointINSTANCE = FfiConverterEndpoint{} - -func (c FfiConverterEndpoint) Lift(pointer unsafe.Pointer) *Endpoint { - result := &Endpoint{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_endpoint(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_endpoint(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*Endpoint).Destroy) - return result -} - -func (c FfiConverterEndpoint) Read(reader io.Reader) *Endpoint { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterEndpoint) Lower(value *Endpoint) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Endpoint") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterEndpoint) Write(writer io.Writer, value *Endpoint) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerEndpoint struct{} - -func (_ FfiDestroyerEndpoint) Destroy(value *Endpoint) { - value.Destroy() -} - // A filter for subscriptions and iteration. type FilterInterface interface { // Restrict to the global namespace, no per stream data. @@ -2562,334 +2209,6 @@ func (_ FfiDestroyerPublicKey) Destroy(value *PublicKey) { value.Destroy() } -type ReceiverInterface interface { - NodeAddr() *NodeAddr - // Subscribe to the given topic on the remote. - Subscribe(remoteId *PublicKey, topic string) error - // Unsubscribe from this topic on the remote. - Unsubscribe(remoteId *PublicKey, topic string) error -} -type Receiver struct { - ffiObject FfiObject -} - -// Create a new receiver. -func NewReceiver(endpoint *Endpoint, handler DataHandlerOld) (*Receiver, error) { - res, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *Receiver { - return FfiConverterReceiverINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_constructor_receiver_new(FfiConverterEndpointINSTANCE.Lower(endpoint), FfiConverterDataHandlerOldINSTANCE.Lower(handler)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - if err == nil { - return res, nil - } - - return res, err -} - -func (_self *Receiver) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Receiver") - defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[error]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_method_receiver_node_addr( - _pointer), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res -} - -// Subscribe to the given topic on the remote. -func (_self *Receiver) Subscribe(remoteId *PublicKey, topic string) error { - _pointer := _self.ffiObject.incrementPointer("*Receiver") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_receiver_subscribe( - _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - - if err == nil { - return nil - } - - return err -} - -// Unsubscribe from this topic on the remote. -func (_self *Receiver) Unsubscribe(remoteId *PublicKey, topic string) error { - _pointer := _self.ffiObject.incrementPointer("*Receiver") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_receiver_unsubscribe( - _pointer, FfiConverterPublicKeyINSTANCE.Lower(remoteId), FfiConverterStringINSTANCE.Lower(topic)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - - if err == nil { - return nil - } - - return err -} -func (object *Receiver) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterReceiver struct{} - -var FfiConverterReceiverINSTANCE = FfiConverterReceiver{} - -func (c FfiConverterReceiver) Lift(pointer unsafe.Pointer) *Receiver { - result := &Receiver{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_receiver(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_receiver(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*Receiver).Destroy) - return result -} - -func (c FfiConverterReceiver) Read(reader io.Reader) *Receiver { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterReceiver) Lower(value *Receiver) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Receiver") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterReceiver) Write(writer io.Writer, value *Receiver) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerReceiver struct{} - -func (_ FfiDestroyerReceiver) Destroy(value *Receiver) { - value.Destroy() -} - -type SenderInterface interface { - NodeAddr() *NodeAddr - // Sends the given data to all subscribers that have subscribed to this `key`. - Send(key string, data []byte) error -} -type Sender struct { - ffiObject FfiObject -} - -// Create a new sender. -func NewSender(endpoint *Endpoint) *Sender { - res, _ := uniffiRustCallAsync[error]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *Sender { - return FfiConverterSenderINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_constructor_sender_new(FfiConverterEndpointINSTANCE.Lower(endpoint)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res -} - -func (_self *Sender) NodeAddr() *NodeAddr { - _pointer := _self.ffiObject.incrementPointer("*Sender") - defer _self.ffiObject.decrementPointer() - res, _ := uniffiRustCallAsync[error]( - nil, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer { - res := C.ffi_iroh_streamplace_rust_future_complete_pointer(handle, status) - return res - }, - // liftFn - func(ffi unsafe.Pointer) *NodeAddr { - return FfiConverterNodeAddrINSTANCE.Lift(ffi) - }, - C.uniffi_iroh_streamplace_fn_method_sender_node_addr( - _pointer), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_pointer(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_pointer(handle) - }, - ) - - return res -} - -// Sends the given data to all subscribers that have subscribed to this `key`. -func (_self *Sender) Send(key string, data []byte) error { - _pointer := _self.ffiObject.incrementPointer("*Sender") - defer _self.ffiObject.decrementPointer() - _, err := uniffiRustCallAsync[Error]( - FfiConverterErrorINSTANCE, - // completeFn - func(handle C.uint64_t, status *C.RustCallStatus) struct{} { - C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) - return struct{}{} - }, - // liftFn - func(_ struct{}) struct{} { return struct{}{} }, - C.uniffi_iroh_streamplace_fn_method_sender_send( - _pointer, FfiConverterStringINSTANCE.Lower(key), FfiConverterBytesINSTANCE.Lower(data)), - // pollFn - func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) - }, - // freeFn - func(handle C.uint64_t) { - C.ffi_iroh_streamplace_rust_future_free_void(handle) - }, - ) - - if err == nil { - return nil - } - - return err -} -func (object *Sender) Destroy() { - runtime.SetFinalizer(object, nil) - object.ffiObject.destroy() -} - -type FfiConverterSender struct{} - -var FfiConverterSenderINSTANCE = FfiConverterSender{} - -func (c FfiConverterSender) Lift(pointer unsafe.Pointer) *Sender { - result := &Sender{ - newFfiObject( - pointer, - func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer { - return C.uniffi_iroh_streamplace_fn_clone_sender(pointer, status) - }, - func(pointer unsafe.Pointer, status *C.RustCallStatus) { - C.uniffi_iroh_streamplace_fn_free_sender(pointer, status) - }, - ), - } - runtime.SetFinalizer(result, (*Sender).Destroy) - return result -} - -func (c FfiConverterSender) Read(reader io.Reader) *Sender { - return c.Lift(unsafe.Pointer(uintptr(readUint64(reader)))) -} - -func (c FfiConverterSender) Lower(value *Sender) unsafe.Pointer { - // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here, - // because the pointer will be decremented immediately after this function returns, - // and someone will be left holding onto a non-locked pointer. - pointer := value.ffiObject.incrementPointer("*Sender") - defer value.ffiObject.decrementPointer() - return pointer - -} - -func (c FfiConverterSender) Write(writer io.Writer, value *Sender) { - writeUint64(writer, uint64(uintptr(c.Lower(value)))) -} - -type FfiDestroyerSender struct{} - -func (_ FfiDestroyerSender) Destroy(value *Sender) { - value.Destroy() -} - // A response to a subscribe request. // // This can be used as a stream of [`SubscribeItem`]s. diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 49e04249..7f735d8a 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -391,20 +391,6 @@ static void call_UniffiCallbackInterfaceDataHandlerMethod0( } -#endif -#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_DATA_HANDLER_OLD_METHOD0 -#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_DATA_HANDLER_OLD_METHOD0 -typedef void (*UniffiCallbackInterfaceDataHandlerOldMethod0)(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); - -// Making function static works arround: -// https://github.com/golang/go/issues/11263 -static void call_UniffiCallbackInterfaceDataHandlerOldMethod0( - UniffiCallbackInterfaceDataHandlerOldMethod0 cb, uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return) -{ - return cb(uniffi_handle, topic, data, uniffi_future_callback, uniffi_callback_data, uniffi_out_return); -} - - #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_GO_SIGNER_METHOD0 @@ -427,14 +413,6 @@ typedef struct UniffiVTableCallbackInterfaceDataHandler { UniffiCallbackInterfaceFree uniffiFree; } UniffiVTableCallbackInterfaceDataHandler; -#endif -#ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER_OLD -#define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_DATA_HANDLER_OLD -typedef struct UniffiVTableCallbackInterfaceDataHandlerOld { - UniffiCallbackInterfaceDataHandlerOldMethod0 handleData; - UniffiCallbackInterfaceFree uniffiFree; -} UniffiVTableCallbackInterfaceDataHandlerOld; - #endif #ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER #define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_GO_SIGNER @@ -464,26 +442,6 @@ void uniffi_iroh_streamplace_fn_init_callback_vtable_datahandler(UniffiVTableCal uint64_t uniffi_iroh_streamplace_fn_method_datahandler_handle_data(void* ptr, RustBuffer topic, RustBuffer data ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLEROLD -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DATAHANDLEROLD -void* uniffi_iroh_streamplace_fn_clone_datahandlerold(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DATAHANDLEROLD -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_DATAHANDLEROLD -void uniffi_iroh_streamplace_fn_free_datahandlerold(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_DATAHANDLEROLD -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_INIT_CALLBACK_VTABLE_DATAHANDLEROLD -void uniffi_iroh_streamplace_fn_init_callback_vtable_datahandlerold(UniffiVTableCallbackInterfaceDataHandlerOld* vtable -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DATAHANDLEROLD_HANDLE_DATA -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_DATAHANDLEROLD_HANDLE_DATA -uint64_t uniffi_iroh_streamplace_fn_method_datahandlerold_handle_data(void* ptr, RustBuffer topic, RustBuffer data -); -#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DB #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_DB void* uniffi_iroh_streamplace_fn_clone_db(void* ptr, RustCallStatus *out_status @@ -514,27 +472,6 @@ void* uniffi_iroh_streamplace_fn_method_db_subscribe_with_opts(void* ptr, RustBu void* uniffi_iroh_streamplace_fn_method_db_write(void* ptr, RustBuffer secret, RustCallStatus *out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_ENDPOINT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_ENDPOINT -void* uniffi_iroh_streamplace_fn_clone_endpoint(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_ENDPOINT -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_ENDPOINT -void uniffi_iroh_streamplace_fn_free_endpoint(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_ENDPOINT_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_ENDPOINT_NEW -uint64_t uniffi_iroh_streamplace_fn_constructor_endpoint_new(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_ENDPOINT_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_ENDPOINT_NODE_ADDR -uint64_t uniffi_iroh_streamplace_fn_method_endpoint_node_addr(void* ptr -); -#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_FILTER #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_FILTER void* uniffi_iroh_streamplace_fn_clone_filter(void* ptr, RustCallStatus *out_status @@ -746,61 +683,6 @@ RustBuffer uniffi_iroh_streamplace_fn_method_publickey_fmt_short(void* ptr, Rust RustBuffer uniffi_iroh_streamplace_fn_method_publickey_uniffi_trait_display(void* ptr, RustCallStatus *out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_RECEIVER -void* uniffi_iroh_streamplace_fn_clone_receiver(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_RECEIVER -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_RECEIVER -void uniffi_iroh_streamplace_fn_free_receiver(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_RECEIVER_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_RECEIVER_NEW -uint64_t uniffi_iroh_streamplace_fn_constructor_receiver_new(void* endpoint, void* handler -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_NODE_ADDR -uint64_t uniffi_iroh_streamplace_fn_method_receiver_node_addr(void* ptr -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_SUBSCRIBE -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_SUBSCRIBE -uint64_t uniffi_iroh_streamplace_fn_method_receiver_subscribe(void* ptr, void* remote_id, RustBuffer topic -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_UNSUBSCRIBE -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_RECEIVER_UNSUBSCRIBE -uint64_t uniffi_iroh_streamplace_fn_method_receiver_unsubscribe(void* ptr, void* remote_id, RustBuffer topic -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SENDER -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SENDER -void* uniffi_iroh_streamplace_fn_clone_sender(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_SENDER -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_FREE_SENDER -void uniffi_iroh_streamplace_fn_free_sender(void* ptr, RustCallStatus *out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_SENDER_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CONSTRUCTOR_SENDER_NEW -uint64_t uniffi_iroh_streamplace_fn_constructor_sender_new(void* endpoint -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SENDER_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SENDER_NODE_ADDR -uint64_t uniffi_iroh_streamplace_fn_method_sender_node_addr(void* ptr -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SENDER_SEND -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_SENDER_SEND -uint64_t uniffi_iroh_streamplace_fn_method_sender_send(void* ptr, RustBuffer key, RustBuffer data -); -#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SUBSCRIBERESPONSE #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_CLONE_SUBSCRIBERESPONSE void* uniffi_iroh_streamplace_fn_clone_subscriberesponse(void* ptr, RustCallStatus *out_status @@ -1153,12 +1035,6 @@ uint16_t uniffi_iroh_streamplace_checksum_func_subscribe_item_debug(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLER_HANDLE_DATA uint16_t uniffi_iroh_streamplace_checksum_method_datahandler_handle_data(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLEROLD_HANDLE_DATA -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DATAHANDLEROLD_HANDLE_DATA -uint16_t uniffi_iroh_streamplace_checksum_method_datahandlerold_handle_data(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_ITER_WITH_OPTS @@ -1183,12 +1059,6 @@ uint16_t uniffi_iroh_streamplace_checksum_method_db_subscribe_with_opts(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_DB_WRITE uint16_t uniffi_iroh_streamplace_checksum_method_db_write(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_ENDPOINT_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_ENDPOINT_NODE_ADDR -uint16_t uniffi_iroh_streamplace_checksum_method_endpoint_node_addr(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_FILTER_GLOBAL @@ -1327,36 +1197,6 @@ uint16_t uniffi_iroh_streamplace_checksum_method_publickey_equal(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_PUBLICKEY_FMT_SHORT uint16_t uniffi_iroh_streamplace_checksum_method_publickey_fmt_short(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_NODE_ADDR -uint16_t uniffi_iroh_streamplace_checksum_method_receiver_node_addr(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_SUBSCRIBE -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_SUBSCRIBE -uint16_t uniffi_iroh_streamplace_checksum_method_receiver_subscribe(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_UNSUBSCRIBE -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_RECEIVER_UNSUBSCRIBE -uint16_t uniffi_iroh_streamplace_checksum_method_receiver_unsubscribe(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SENDER_NODE_ADDR -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SENDER_NODE_ADDR -uint16_t uniffi_iroh_streamplace_checksum_method_sender_node_addr(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SENDER_SEND -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SENDER_SEND -uint16_t uniffi_iroh_streamplace_checksum_method_sender_send(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_SUBSCRIBERESPONSE_NEXT_RAW @@ -1369,12 +1209,6 @@ uint16_t uniffi_iroh_streamplace_checksum_method_subscriberesponse_next_raw(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_WRITESCOPE_PUT uint16_t uniffi_iroh_streamplace_checksum_method_writescope_put(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_ENDPOINT_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_ENDPOINT_NEW -uint16_t uniffi_iroh_streamplace_checksum_constructor_endpoint_new(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_FILTER_NEW @@ -1417,18 +1251,6 @@ uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_bytes(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_PUBLICKEY_FROM_STRING uint16_t uniffi_iroh_streamplace_checksum_constructor_publickey_from_string(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_RECEIVER_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_RECEIVER_NEW -uint16_t uniffi_iroh_streamplace_checksum_constructor_receiver_new(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_SENDER_NEW -#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_CONSTRUCTOR_SENDER_NEW -uint16_t uniffi_iroh_streamplace_checksum_constructor_sender_new(void - ); #endif #ifndef UNIFFI_FFIDEF_FFI_IROH_STREAMPLACE_UNIFFI_CONTRACT_VERSION @@ -1440,8 +1262,6 @@ uint32_t ffi_iroh_streamplace_uniffi_contract_version(void void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerMethod0(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerFree(uint64_t handle); - void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldMethod0(uint64_t uniffi_handle, RustBuffer topic, RustBuffer data, UniffiForeignFutureCompleteVoid uniffi_future_callback, uint64_t uniffi_callback_data, UniffiForeignFuture* uniffi_out_return); - void iroh_streamplace_cgo_dispatchCallbackInterfaceDataHandlerOldFree(uint64_t handle); void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerMethod0(uint64_t uniffi_handle, RustBuffer data, RustBuffer* uniffi_out_return, RustCallStatus* callStatus ); void iroh_streamplace_cgo_dispatchCallbackInterfaceGoSignerFree(uint64_t handle); diff --git a/pkg/iroh/iroh_streamplace_test.go b/pkg/iroh/iroh_streamplace_test.go deleted file mode 100644 index 41736876..00000000 --- a/pkg/iroh/iroh_streamplace_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package iroh_streamplace - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - iroh "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" -) - -type Message struct { - topic string - data []byte -} - -type TestHandler struct { - messages chan Message -} - -func (handler TestHandler) HandleData(topic string, data []byte) { - handler.messages <- Message{topic, data} -} - -func TestBasicRoundtrip(t *testing.T) { - ep1, err := iroh.NewEndpoint() - assert.Nil(t, err) - sender := iroh.NewSender(ep1) - - messages := make(chan Message, 5) - handler := TestHandler{messages: messages} - - ep2, err := iroh.NewEndpoint() - assert.NoError(t, err) - receiver, err := iroh.NewReceiver(ep2, &handler) - assert.NoError(t, err) - - senderAddr := sender.NodeAddr() - senderId := senderAddr.NodeId() - - // subscribe - err = receiver.Subscribe(senderId, "foo") - assert.NoError(t, err) - - // send a few messages - for i := range 5 { - err = sender.Send("foo", []byte{byte(i), 0, 0, 0}) - assert.NoError(t, err) - } - - // make sure the receiver got them - for i := range 5 { - msg := <-messages - assert.Equal(t, msg.topic, "foo") - assert.Equal(t, msg.data, []byte{byte(i), 0, 0, 0}) - } -} diff --git a/pkg/media/media.go b/pkg/media/media.go index a4240927..d9f71f5e 100644 --- a/pkg/media/media.go +++ b/pkg/media/media.go @@ -58,6 +58,7 @@ type NewSegmentNotification struct { Segment *model.Segment Data []byte Metadata *SegmentMetadata + Local bool } func RunSelfTest(ctx context.Context) error { @@ -65,7 +66,7 @@ func RunSelfTest(ctx context.Context) error { return SelfTest(ctx) } -func MakeMediaManager(ctx context.Context, cli *config.CLI, signer crypto.Signer, rep replication.Replicator, mod model.Model, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer) (*MediaManager, error) { +func MakeMediaManager(ctx context.Context, cli *config.CLI, signer crypto.Signer, mod model.Model, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer) (*MediaManager, error) { gstinit.InitGST() err := SelfTest(ctx) if err != nil { @@ -121,7 +122,6 @@ func MakeMediaManager(ctx context.Context, cli *config.CLI, signer crypto.Signer return &MediaManager{ cli: cli, - replicator: rep, hlsRunning: map[string]*M3U8{}, httpPipes: map[string]io.Writer{}, model: mod, @@ -135,7 +135,7 @@ func MakeMediaManager(ctx context.Context, cli *config.CLI, signer crypto.Signer func (mm *MediaManager) HandleData(node *irohStreamplace.PublicKey, data []byte) { r := bytes.NewReader(data) ctx := context.Background() - err := mm.ValidateMP4(ctx, r) + err := mm.ValidateMP4(ctx, r, true) if err != nil { log.Log(ctx, "invalid incoming segment", "error", err) } diff --git a/pkg/media/media_test.go b/pkg/media/media_test.go index 9883e54f..13f875aa 100644 --- a/pkg/media/media_test.go +++ b/pkg/media/media_test.go @@ -13,7 +13,6 @@ import ( "stream.place/streamplace/pkg/config" ct "stream.place/streamplace/pkg/config/configtesting" "stream.place/streamplace/pkg/model" - "stream.place/streamplace/pkg/replication/boring" ) func getFixture(name string) string { @@ -40,7 +39,7 @@ func getStaticTestMediaManager(t *testing.T) (*MediaManager, MediaSigner) { StatefulDB: nil, // Test doesn't need StatefulDB for now Bus: bus.NewBus(), } - mm, err := MakeMediaManager(context.Background(), cli, nil, &boring.BoringReplicator{}, mod, bus.NewBus(), atsync) + mm, err := MakeMediaManager(context.Background(), cli, nil, mod, bus.NewBus(), atsync) require.NoError(t, err) // ms, err := MakeMediaSigner(context.Background(), cli, "test-person", signer) // require.NoError(t, err) @@ -126,6 +125,6 @@ func TestVerifyMP4(t *testing.T) { f, err := os.Open(getFixture("sample-segment.mp4")) require.NoError(t, err) mm, _ := getStaticTestMediaManager(t) - err = mm.ValidateMP4(context.Background(), f) + err = mm.ValidateMP4(context.Background(), f, true) require.NoError(t, err) } diff --git a/pkg/media/rtcrec_test.go b/pkg/media/rtcrec_test.go index a495a89d..de5348ef 100644 --- a/pkg/media/rtcrec_test.go +++ b/pkg/media/rtcrec_test.go @@ -11,7 +11,6 @@ import ( "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/crypto/spkey" "stream.place/streamplace/pkg/globalerror" - "stream.place/streamplace/pkg/replication/boring" "stream.place/streamplace/pkg/rtcrec" ) @@ -26,7 +25,7 @@ func TestRTCRecording(t *testing.T) { fs := cli.NewFlagSet("rtcrec-test") err = cli.Parse(fs, []string{"--data-dir", dir, "-wide-open=true"}) require.NoError(t, err) - mm, err := MakeMediaManager(context.Background(), cli, nil, &boring.BoringReplicator{}, nil, nil, nil) + mm, err := MakeMediaManager(context.Background(), cli, nil, nil, nil, nil) require.NoError(t, err) priv, pub, err := spkey.GenerateStreamKey() require.NoError(t, err) diff --git a/pkg/media/segmenter.go b/pkg/media/segmenter.go index 485e2a21..0aedd714 100644 --- a/pkg/media/segmenter.go +++ b/pkg/media/segmenter.go @@ -87,7 +87,7 @@ func (mm *MediaManager) SegmentAndSignElem(ctx context.Context, ms MediaSigner) return } - err = mm.ValidateMP4(ctx, bytes.NewReader(bs)) + err = mm.ValidateMP4(ctx, bytes.NewReader(bs), true) if err != nil { log.Error(ctx, "error validating segment", "error", err) globalerror.GlobalError(err) diff --git a/pkg/media/validate.go b/pkg/media/validate.go index df17c39a..01223f1c 100644 --- a/pkg/media/validate.go +++ b/pkg/media/validate.go @@ -23,7 +23,7 @@ type ManifestAndCert struct { Cert string `json:"cert"` } -func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error { +func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader, local bool) error { ctx, span := otel.Tracer("signer").Start(ctx, "ValidateMP4") defer span.End() buf, err := io.ReadAll(input) @@ -82,7 +82,7 @@ func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error return err } defer fd.Close() - go mm.replicator.NewSegment(ctx, buf) + r := bytes.NewReader(buf) if _, err := io.Copy(fd, r); err != nil { return err @@ -102,6 +102,7 @@ func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error Segment: seg, Data: buf, Metadata: meta, + Local: local, } for _, ch := range mm.newSegmentSubs { go func() { ch <- not }() diff --git a/pkg/replication/iroh_replicator/iroh.go b/pkg/replication/iroh_replicator/iroh.go index 138ab0ea..d389744e 100644 --- a/pkg/replication/iroh_replicator/iroh.go +++ b/pkg/replication/iroh_replicator/iroh.go @@ -2,36 +2,17 @@ package iroh_replicator import ( "context" - - "stream.place/streamplace/pkg/log" - - irohStreamplace "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" ) // IrohReplicator implements the replication mechanism using iroh type IrohReplicator struct { - topic string - sender *irohStreamplace.Sender } -func NewIrohReplicator(ctx context.Context, ep *irohStreamplace.Endpoint, topic string) (*IrohReplicator, error) { - sender := irohStreamplace.NewSender(ep) +func NewIrohReplicator(ctx context.Context) (*IrohReplicator, error) { - return &IrohReplicator{ - topic: topic, - sender: sender, - }, nil + return &IrohReplicator{}, nil } func (rep *IrohReplicator) NewSegment(ctx context.Context, bs []byte) { - go func(topic string) { - err := sendSegment(rep.sender, topic, bs) - if err != nil { - log.Log(ctx, "error replicating segment", "error", err) - } - }(rep.topic) -} -func sendSegment(endpoint *irohStreamplace.Sender, topic string, bs []byte) error { - return endpoint.Send(topic, bs) } diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go index e11d63bb..a804006f 100644 --- a/pkg/replication/iroh_replicator/kv.go +++ b/pkg/replication/iroh_replicator/kv.go @@ -1,19 +1,26 @@ package iroh_replicator import ( + "bytes" "context" "encoding/json" "fmt" "time" + "github.com/bluesky-social/indigo/util" + "golang.org/x/sync/errgroup" "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" "stream.place/streamplace/pkg/log" + "stream.place/streamplace/pkg/media" ) -type SwarmKV struct { - Node *iroh_streamplace.Node - DB *iroh_streamplace.Db - w *iroh_streamplace.WriteScope +type IrohSwarm struct { + Node *iroh_streamplace.Node + DB *iroh_streamplace.Db + w *iroh_streamplace.WriteScope + mm *media.MediaManager + segChan chan *media.NewSegmentNotification + nodeId string } // A message saying "hey I ingested node data at this time" @@ -22,14 +29,7 @@ type OriginInfo struct { Time string `json:"time"` } -type DataHandler struct{} - -func (handler *DataHandler) HandleData(topic string, data []byte) { - log.Log(context.Background(), "HandleData", "topic", topic, "data", len(data)) -} - -func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, error) { - handler := &DataHandler{} +func NewSwarm(ctx context.Context, tickets []string, secret []byte, mm *media.MediaManager) (*IrohSwarm, error) { ctx = log.WithLogValues(ctx, "func", "StartKV") log.Log(ctx, "Starting with tickets", "tickets", tickets) @@ -39,7 +39,12 @@ func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, er MaxSendDuration: 1000_000_000, // 1s } log.Log(ctx, "Config created", "config", config) - node, err := iroh_streamplace.NodeReceiver(config, handler) + + swarm := IrohSwarm{ + mm: mm, + } + + node, err := iroh_streamplace.NodeReceiver(config, &swarm) if err != nil { return nil, fmt.Errorf("failed to create NodeSender: %w", err) } @@ -47,11 +52,16 @@ func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, er db := node.Db() w := node.NodeScope() - node_id, err := node.NodeId() + swarm.DB = db + swarm.w = w + swarm.Node = node + + nodeId, err := node.NodeId() if err != nil { return nil, fmt.Errorf("failed to get NodeId: %w", err) } - log.Log(ctx, "Node ID:", "node_id", node_id) + log.Log(ctx, "Node ID:", "node_id", nodeId) + swarm.nodeId = nodeId.String() ticket, err := node.Ticket() if err != nil { @@ -59,17 +69,12 @@ func StartKV(ctx context.Context, tickets []string, secret []byte) (*SwarmKV, er } log.Log(ctx, "Ticket:", "ticket", ticket) - swarm := SwarmKV{ - Node: node, - DB: db, - w: w, - } return &swarm, nil } var activeSubs = make(map[string]bool) -func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { +func (swarm *IrohSwarm) Start(ctx context.Context, tickets []string) error { if len(tickets) > 0 { err := swarm.Node.JoinPeers(tickets) if err != nil { @@ -84,6 +89,17 @@ func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { nodeIdStr := nodeId.String() log.Log(ctx, "Node ID:", "node_id", nodeIdStr) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return swarm.startKV(ctx) + }) + g.Go(func() error { + return swarm.startSegmentSender(ctx) + }) + return g.Wait() +} + +func (swarm *IrohSwarm) startKV(ctx context.Context) error { sub := swarm.DB.Subscribe(iroh_streamplace.NewFilter()) for { if ctx.Err() != nil { @@ -110,7 +126,7 @@ func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { continue } if !activeSubs[keyStr] { - if info.NodeID == nodeIdStr { + if info.NodeID == swarm.nodeId { activeSubs[keyStr] = true continue } @@ -137,8 +153,52 @@ func (swarm *SwarmKV) Start(ctx context.Context, tickets []string) error { } } -func (swarm *SwarmKV) Put(ctx context.Context, key string, value []byte) error { - // streamerBs := []byte(streamer) - keyBs := []byte(key) - return swarm.w.Put(nil, keyBs, value) +func (swarm *IrohSwarm) startSegmentSender(ctx context.Context) error { + ch := swarm.mm.NewSegment() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case not := <-ch: + err := swarm.SendSegment(ctx, not) + if err != nil { + log.Error(ctx, "could not send segment to swarm", "error", err) + } + continue + } + } +} + +func (swarm *IrohSwarm) HandleData(topic string, data []byte) { + err := swarm.mm.ValidateMP4(context.Background(), bytes.NewReader(data), false) + if err != nil { + log.Error(context.Background(), "could not validate segment", "error", err) + } +} + +func (swarm *IrohSwarm) SendSegment(ctx context.Context, not *media.NewSegmentNotification) error { + if !not.Local { + return nil + } + originInfo := OriginInfo{ + NodeID: swarm.nodeId, + Time: not.Segment.StartTime.Format(util.ISO8601), + } + bs, err := json.Marshal(originInfo) + if err != nil { + log.Error(ctx, "could not marshal origin info", "error", err) + return err + } + keyBs := []byte(not.Segment.RepoDID) + err = swarm.w.Put(nil, keyBs, bs) + if err != nil { + log.Error(ctx, "could not put segment to swarm", "error", err) + return err + } + err = swarm.Node.SendSegment(not.Segment.RepoDID, not.Data) + if err != nil { + log.Error(ctx, "could not send segment to swarm", "error", err) + return err + } + return nil } diff --git a/rust/iroh-streamplace/src/api.rs b/rust/iroh-streamplace/src/api.rs deleted file mode 100644 index 677e71ec..00000000 --- a/rust/iroh-streamplace/src/api.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Protocol API - -use std::collections::{BTreeMap, BTreeSet}; - -use bytes::Bytes; -use iroh::{Endpoint, NodeId, protocol::ProtocolHandler}; -use irpc::{Client, WithChannels, channel::oneshot, rpc::RemoteService, rpc_requests}; -use irpc_iroh::{IrohProtocol, IrohRemoteConnection}; -use n0_future::future::Boxed; -use serde::{Deserialize, Serialize}; -use tracing::{debug, warn}; - -/// Subscribe to the given `key` -#[derive(Debug, Serialize, Deserialize)] -struct Subscribe { - key: String, - // TODO: verify - remote_id: NodeId, -} - -/// Unsubscribe from the given `key` -#[derive(Debug, Serialize, Deserialize)] -struct Unsubscribe { - key: String, - // TODO: verify - remote_id: NodeId, -} - -#[derive(Debug, Serialize, Deserialize)] -struct SendSegment { - key: String, - data: Bytes, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct RecvSegment { - key: String, - data: Bytes, -} - -// Use the macro to generate both the Protocol and Message enums -// plus implement Channels for each type -#[rpc_requests(message = Message)] -#[derive(Serialize, Deserialize, Debug)] -enum Protocol { - #[rpc(tx=oneshot::Sender<()>)] - Subscribe(Subscribe), - #[rpc(tx=oneshot::Sender<()>)] - Unsubscribe(Unsubscribe), - #[rpc(tx=oneshot::Sender<()>)] - SendSegment(SendSegment), - #[rpc(tx=oneshot::Sender<()>)] - RecvSegment(RecvSegment), -} - -struct Actor { - endpoint: iroh::Endpoint, - recv: tokio::sync::mpsc::Receiver, - subscriptions: BTreeMap>, - connections: BTreeMap, - handler: Box) -> Boxed<()> + Send + Sync + 'static>, -} - -#[derive(Debug)] -struct Connection { - _id: NodeId, - rpc: Client, -} - -impl Actor { - fn spawn( - endpoint: &iroh::Endpoint, - handler: impl Fn(String, Vec) -> Boxed<()> + Send + Sync + 'static, - ) -> Api { - let (tx, rx) = tokio::sync::mpsc::channel(1); - let actor = Self { - endpoint: endpoint.clone(), - recv: rx, - subscriptions: BTreeMap::new(), - connections: BTreeMap::new(), - handler: Box::new(handler), - }; - n0_future::task::spawn(actor.run()); - Api { - inner: Client::local(tx), - } - } - - async fn run(mut self) { - while let Some(msg) = self.recv.recv().await { - self.handle(msg).await; - } - } - - async fn handle(&mut self, msg: Message) { - match msg { - Message::Subscribe(sub) => { - debug!("subscribe {:?}", sub); - let WithChannels { tx, inner, .. } = sub; - - self.subscriptions - .entry(inner.key) - .or_default() - .insert(inner.remote_id); - - tx.send(()).await.ok(); - } - Message::Unsubscribe(sub) => { - debug!("unsubscribe {:?}", sub); - let WithChannels { tx, inner, .. } = sub; - - if let Some(e) = self.subscriptions.get_mut(&inner.key) { - e.remove(&inner.remote_id); - } - - tx.send(()).await.ok(); - } - Message::SendSegment(segment) => { - debug!("send segment {:?}", segment); - let WithChannels { tx, inner, .. } = segment; - - let msg = RecvSegment { - key: inner.key.clone(), - data: inner.data.clone(), - }; - - for (key, remotes) in &self.subscriptions { - if key == &inner.key { - for remote in remotes { - debug!("sending to topic {}: {}", key, remote); - - // ensure connection - if !self.connections.contains_key(remote) { - let conn = IrohRemoteConnection::new( - self.endpoint.clone(), - (*remote).into(), - Api::ALPN.to_vec(), - ); - - let conn = Connection { - rpc: Client::boxed(conn), - _id: *remote, - }; - self.connections.insert(*remote, conn); - } - let conn = self.connections.get(remote).expect("just checked"); - - if let Err(err) = conn.rpc.rpc(msg.clone()).await { - warn!("failed to send to {}: {:?}", remote, err); - // remove conn - self.connections.remove(remote); - } - } - } - } - - tx.send(()).await.ok(); - } - Message::RecvSegment(segment) => { - debug!("recv segment {:?}", segment); - let WithChannels { tx, inner, .. } = segment; - (self.handler)(inner.key, inner.data.to_vec()).await; - tx.send(()).await.ok(); - } - } - } -} - -/// The actual API to interact with -pub(crate) struct Api { - inner: Client, -} - -impl Api { - pub(crate) const ALPN: &[u8] = b"/iroh/streamplace/1"; - - pub(crate) fn spawn(endpoint: &iroh::Endpoint) -> Self { - Actor::spawn(endpoint, |_, _| Box::pin(async move {})) - } - - pub(crate) fn spawn_with_handler( - endpoint: &iroh::Endpoint, - handler: impl Fn(String, Vec) -> Boxed<()> + Send + Sync + 'static, - ) -> Self { - Actor::spawn(endpoint, handler) - } - - pub(crate) fn connect(endpoint: Endpoint, addr: impl Into) -> Api { - let conn = IrohRemoteConnection::new(endpoint, addr.into(), Self::ALPN.to_vec()); - Api { - inner: Client::boxed(conn), - } - } - - pub(crate) fn expose(&self) -> impl ProtocolHandler { - let local = self - .inner - .as_local() - .expect("can not listen on remote service"); - IrohProtocol::new(Protocol::remote_handler(local)) - } - - pub(crate) async fn subscribe(&self, key: String, self_id: NodeId) -> irpc::Result<()> { - self.inner - .rpc(Subscribe { - key, - remote_id: self_id, - }) - .await - } - - pub(crate) async fn unsubscribe(&self, key: String, self_id: NodeId) -> irpc::Result<()> { - self.inner - .rpc(Unsubscribe { - key, - remote_id: self_id, - }) - .await - } - - /// Send this segment to all subscriptions. - pub(crate) async fn send_segment(&self, key: String, data: Bytes) -> irpc::Result<()> { - let msg = SendSegment { key, data }; - self.inner.rpc(msg).await - } -} diff --git a/rust/iroh-streamplace/src/endpoint.rs b/rust/iroh-streamplace/src/endpoint.rs deleted file mode 100644 index b3662d2e..00000000 --- a/rust/iroh-streamplace/src/endpoint.rs +++ /dev/null @@ -1,30 +0,0 @@ -use iroh::Watcher; - -use crate::{error::Error, node_addr::NodeAddr}; - -#[derive(uniffi::Object, Debug, Clone)] -pub struct Endpoint { - pub(crate) endpoint: iroh::Endpoint, -} - -#[uniffi::export] -impl Endpoint { - /// Create a new endpoint. - #[uniffi::constructor(async_runtime = "tokio")] - pub async fn new() -> Result { - let endpoint = iroh::Endpoint::builder() - .discovery_n0() - .discovery_local_network() - .bind() - .await?; - - Ok(Self { endpoint }) - } - - #[uniffi::method(async_runtime = "tokio")] - pub async fn node_addr(&self) -> NodeAddr { - let _ = self.endpoint.home_relay().initialized().await; - let addr = self.endpoint.node_addr().initialized().await; - addr.into() - } -} diff --git a/rust/iroh-streamplace/src/lib.rs b/rust/iroh-streamplace/src/lib.rs index 52558b38..6297bfb5 100644 --- a/rust/iroh-streamplace/src/lib.rs +++ b/rust/iroh-streamplace/src/lib.rs @@ -1,12 +1,7 @@ uniffi::setup_scaffolding!(); pub mod c2pa; -pub mod endpoint; pub mod error; -pub mod public_key; pub mod node; -pub mod receiver; -pub mod sender; pub mod node_addr; - -mod api; +pub mod public_key; diff --git a/rust/iroh-streamplace/src/receiver.rs b/rust/iroh-streamplace/src/receiver.rs deleted file mode 100644 index 23d2df7a..00000000 --- a/rust/iroh-streamplace/src/receiver.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::sync::Arc; - -use iroh::protocol::Router; - -use crate::{api::Api, endpoint::Endpoint, error::Error, public_key::PublicKey, node_addr::NodeAddr}; - -#[derive(uniffi::Object)] -pub struct Receiver { - endpoint: Endpoint, - _api: Api, - _router: iroh::protocol::Router, -} - -#[uniffi::export] -impl Receiver { - /// Create a new receiver. - #[uniffi::constructor(async_runtime = "tokio")] - pub async fn new( - endpoint: &Endpoint, - handler: Arc, - ) -> Result { - let api = Api::spawn_with_handler(&endpoint.endpoint, move |id, data| { - let handler = handler.clone(); - Box::pin(async move { - handler.handle_data(id, data).await; - }) - }); - let router = Router::builder(endpoint.endpoint.clone()) - .accept(Api::ALPN, api.expose()) - .spawn(); - - Ok(Receiver { - endpoint: endpoint.clone(), - _api: api, - _router: router, - }) - } - - /// Subscribe to the given topic on the remote. - #[uniffi::method(async_runtime = "tokio")] - pub async fn subscribe(&self, remote_id: Arc, topic: &str) -> Result<(), Error> { - let remote_id: iroh::NodeId = remote_id.as_ref().into(); - let api = Api::connect(self.endpoint.endpoint.clone(), remote_id); - api.subscribe(topic.to_string(), self.endpoint.endpoint.node_id()) - .await?; - Ok(()) - } - - /// Unsubscribe from this topic on the remote. - #[uniffi::method(async_runtime = "tokio")] - pub async fn unsubscribe( - &self, - remote_id: Arc, - topic: &str, - ) -> Result<(), Error> { - let remote_id: iroh::NodeId = remote_id.as_ref().into(); - let api = Api::connect(self.endpoint.endpoint.clone(), remote_id); - api.unsubscribe(topic.to_string(), self.endpoint.endpoint.node_id()) - .await?; - Ok(()) - } - - #[uniffi::method(async_runtime = "tokio")] - pub async fn node_addr(&self) -> NodeAddr { - self.endpoint.node_addr().await - } -} - -#[uniffi::export(with_foreign)] -#[async_trait::async_trait] -pub trait DataHandlerOld: Send + Sync { - async fn handle_data(&self, topic: String, data: Vec); -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::sender::Sender; - - #[tokio::test] - async fn test_roundtrip() { - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); - - let ep1 = Endpoint::new().await.unwrap(); - let sender = Sender::new(&ep1).await.unwrap(); - - let (s, mut r) = tokio::sync::mpsc::channel(5); - - #[derive(Debug, Clone)] - struct TestHandler { - messages: tokio::sync::mpsc::Sender<(String, Vec)>, - } - - #[async_trait::async_trait] - impl DataHandlerOld for TestHandler { - async fn handle_data(&self, topic: String, data: Vec) { - self.messages.send((topic, data)).await.unwrap(); - } - } - - let handler = TestHandler { messages: s }; - let ep2 = Endpoint::new().await.unwrap(); - let receiver = Receiver::new(&ep2, Arc::new(handler.clone())) - .await - .unwrap(); - - let sender_addr = sender.node_addr().await; - println!("sender addr: {sender_addr:?}"); - - let receiver_addr = receiver.node_addr().await; - println!("recv addr: {receiver_addr:?}"); - - // subscribe - receiver - .subscribe(Arc::new(sender_addr.node_id()), "foo") - .await - .unwrap(); - - // send a few messages - for i in 0u8..5 { - sender.send("foo", &[i, 0, 0, 0]).await.unwrap(); - } - - // make sure the receiver got them - for i in 0u8..5 { - let (topic, msg) = r.recv().await.unwrap(); - assert_eq!(topic, "foo"); - assert_eq!(msg, vec![i, 0, 0, 0]); - } - - // unsubscribe - receiver - .unsubscribe(Arc::new(sender_addr.node_id()), "foo") - .await - .unwrap(); - - // send a message, shouldn't error - sender.send("foo", &[1]).await.unwrap(); - - // no message received, times out - let res = tokio::time::timeout(std::time::Duration::from_millis(200), async { - r.recv().await.unwrap(); - }) - .await; - assert!(res.is_err()); - } -} diff --git a/rust/iroh-streamplace/src/sender.rs b/rust/iroh-streamplace/src/sender.rs deleted file mode 100644 index 9b665a7f..00000000 --- a/rust/iroh-streamplace/src/sender.rs +++ /dev/null @@ -1,43 +0,0 @@ -use bytes::Bytes; -use iroh::protocol::Router; - -use crate::{api::Api, c2pa::SPError, endpoint::Endpoint, error::Error, node_addr::NodeAddr}; - -#[derive(uniffi::Object)] -pub struct Sender { - endpoint: Endpoint, - api: Api, - _router: iroh::protocol::Router, -} - -#[uniffi::export] -impl Sender { - /// Create a new sender. - #[uniffi::constructor(async_runtime = "tokio")] - pub async fn new(endpoint: &Endpoint) -> Sender { - let api = Api::spawn(&endpoint.endpoint); - let router = Router::builder(endpoint.endpoint.clone()) - .accept(Api::ALPN, api.expose()) - .spawn(); - - Sender { - endpoint: endpoint.clone(), - api, - _router: router, - } - } - - /// Sends the given data to all subscribers that have subscribed to this `key`. - #[uniffi::method(async_runtime = "tokio")] - pub async fn send(&self, key: &str, data: &[u8]) -> Result<(), Error> { - self.api - .send_segment(key.to_string(), Bytes::copy_from_slice(data)) - .await?; - Ok(()) - } - - #[uniffi::method(async_runtime = "tokio")] - pub async fn node_addr(&self) -> NodeAddr { - self.endpoint.node_addr().await - } -} -- 2.51.2 From bcad2c39f845aac7558a060c5931dadd8660657e Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 15:53:54 -0700 Subject: [PATCH 103/525] dev: mv libiroh_streamplace.so instead of cp --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4c170f8f..2d11b979 100644 --- a/Makefile +++ b/Makefile @@ -292,8 +292,10 @@ dev-rust: .build/bin/uniffi-bindgen-go-forked .build/bin/uniffi-bindgen-go-forked --out-dir pkg/iroh/generated --library ./target/debug/libiroh_streamplace.$$EXT \ && mkdir -p $(BUILDDIR)/rust/iroh-streamplace/ \ && mkdir -p $(BUILDDIR)/lib/ \ - && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/rust/iroh-streamplace/ \ - && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/lib/ + && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/rust/iroh-streamplace/libiroh_streamplace.$$EXT.tmp \ + && mv $(BUILDDIR)/rust/iroh-streamplace/libiroh_streamplace.$$EXT.tmp $(BUILDDIR)/rust/iroh-streamplace/libiroh_streamplace.$$EXT \ + && cp ./target/debug/libiroh_streamplace.$$EXT $(BUILDDIR)/lib/libiroh_streamplace.$$EXT.tmp \ + && mv $(BUILDDIR)/lib/libiroh_streamplace.$$EXT.tmp $(BUILDDIR)/lib/libiroh_streamplace.$$EXT .PHONY: dev-test dev-test: -- 2.51.2 From 863d22203af09b55a03f230b0fdb46344070477c Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 16:07:31 -0700 Subject: [PATCH 104/525] ci: go 1.25.1 --- .github/workflows/build.yaml | 2 +- .gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 97777399..7c028351 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -129,7 +129,7 @@ jobs: run: | npm install -g corepack@latest \ && corepack enable \ - && export GOTOOLCHAIN=go1.24.2 \ + && export GOTOOLCHAIN=go1.25.1 \ && brew install go \ && python -m pip install virtualenv \ && python -m virtualenv ~/venv \ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3212da8..033a1033 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -314,7 +314,7 @@ build-android-release: - 'echo "xcodeproj --version: $(xcodeproj --version)"' - "$(which xcodeproj) --version" - > - export GOTOOLCHAIN=go1.24.2 + export GOTOOLCHAIN=go1.25.1 && python3 -m pip install virtualenv && python3 -m virtualenv ~/venv && source ~/venv/bin/activate -- 2.51.2 From 02dde911b9e435017e34106b109d497a6da3e7af Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 17:00:19 -0700 Subject: [PATCH 105/525] iroh: kv cleanup --- pkg/replication/iroh_replicator/kv.go | 67 ++++++++++++++++++--------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go index a804006f..639cb544 100644 --- a/pkg/replication/iroh_replicator/kv.go +++ b/pkg/replication/iroh_replicator/kv.go @@ -15,12 +15,13 @@ import ( ) type IrohSwarm struct { - Node *iroh_streamplace.Node - DB *iroh_streamplace.Db - w *iroh_streamplace.WriteScope - mm *media.MediaManager - segChan chan *media.NewSegmentNotification - nodeId string + Node *iroh_streamplace.Node + DB *iroh_streamplace.Db + w *iroh_streamplace.WriteScope + mm *media.MediaManager + segChan chan *media.NewSegmentNotification + nodeId string + activeSubs map[string]*OriginInfo } // A message saying "hey I ingested node data at this time" @@ -41,7 +42,8 @@ func NewSwarm(ctx context.Context, tickets []string, secret []byte, mm *media.Me log.Log(ctx, "Config created", "config", config) swarm := IrohSwarm{ - mm: mm, + mm: mm, + activeSubs: make(map[string]*OriginInfo), } node, err := iroh_streamplace.NodeReceiver(config, &swarm) @@ -72,8 +74,6 @@ func NewSwarm(ctx context.Context, tickets []string, secret []byte, mm *media.Me return &swarm, nil } -var activeSubs = make(map[string]bool) - func (swarm *IrohSwarm) Start(ctx context.Context, tickets []string) error { if len(tickets) > 0 { err := swarm.Node.JoinPeers(tickets) @@ -110,7 +110,7 @@ func (swarm *IrohSwarm) startKV(ctx context.Context) error { return fmt.Errorf("failed to get next subscription event: %w", err) } if ev == nil { - log.Log(ctx, "Got empty event from sub.NextRaw(), pausing for a second") + log.Debug(ctx, "Got empty event from sub.NextRaw(), pausing for a second") time.Sleep(1 * time.Second) continue } @@ -118,37 +118,60 @@ func (swarm *IrohSwarm) startKV(ctx context.Context) error { case iroh_streamplace.SubscribeItemEntry: keyStr := string(item.Key) valueStr := string(item.Value) - log.Log(ctx, "SubscribeItemEntry", "key", keyStr, "value", valueStr) + log.Debug(ctx, "SubscribeItemEntry", "key", keyStr, "value", valueStr) + if len(keyStr) > 0 && keyStr[0] != '{' { + // not JSON, it's one of the rust messages + continue + } var info OriginInfo err := json.Unmarshal(item.Value, &info) if err != nil { log.Error(ctx, "could not unmarshal origin info", "error", err) continue } - if !activeSubs[keyStr] { - if info.NodeID == swarm.nodeId { - activeSubs[keyStr] = true + oldSub, ok := swarm.activeSubs[keyStr] + if ok { + if oldSub.NodeID == info.NodeID { + // mmyep. same node still has the stream. great news. continue } - pubKey, err := iroh_streamplace.PublicKeyFromString(info.NodeID) + log.Log(ctx, "Stream origin changed, swapping to new node", "old_node", oldSub.NodeID, "new_node", info.NodeID, "streamer", keyStr) + pubKey, err := iroh_streamplace.PublicKeyFromString(oldSub.NodeID) if err != nil { log.Error(ctx, "could not create public key", "error", err) continue } - activeSubs[keyStr] = true - err = swarm.Node.Subscribe(keyStr, pubKey) + // different node has the stream. we need to unsubscribe from the old node. + err = swarm.Node.Unsubscribe(keyStr, pubKey) if err != nil { - log.Error(ctx, "could not subscribe to key", "error", err) + log.Error(ctx, "could not unsubscribe from key", "error", err) continue } + delete(swarm.activeSubs, keyStr) + } + if info.NodeID == swarm.nodeId { + // oh, i have this stream. cool. do nothing. + continue + } + log.Log(ctx, "Subscribing to stream", "new_node", info.NodeID, "streamer", keyStr) + pubKey, err := iroh_streamplace.PublicKeyFromString(info.NodeID) + if err != nil { + log.Error(ctx, "could not create public key", "error", err) + continue + } + err = swarm.Node.Subscribe(keyStr, pubKey) + if err != nil { + log.Error(ctx, "could not subscribe to key", "error", err) + continue } + swarm.activeSubs[keyStr] = &info case iroh_streamplace.SubscribeItemCurrentDone: - log.Log(ctx, "SubscribeItemCurrentDone", "currentDone", item) + log.Debug(ctx, "SubscribeItemCurrentDone", "currentDone", item) case iroh_streamplace.SubscribeItemExpired: - log.Log(ctx, "SubscribeItemExpired", "expired", item) + log.Debug(ctx, "SubscribeItemExpired", "expired", item) case iroh_streamplace.SubscribeItemOther: - log.Log(ctx, "SubscribeItemOther", "other", item) + log.Debug(ctx, "SubscribeItemOther", "other", item) } } } @@ -172,7 +195,7 @@ func (swarm *IrohSwarm) startSegmentSender(ctx context.Context) error { func (swarm *IrohSwarm) HandleData(topic string, data []byte) { err := swarm.mm.ValidateMP4(context.Background(), bytes.NewReader(data), false) if err != nil { - log.Error(context.Background(), "could not validate segment", "error", err) + log.Error(context.Background(), "could not validate segment", "error", err, "topic", topic, "data", len(data)) } } -- 2.51.2 From ece6373097279879434cb2c7b4e9258d306c83ac Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 19:11:58 -0500 Subject: [PATCH 106/525] fix dialog bg being #ffffff --- js/components/src/components/ui/primitives/modal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/components/src/components/ui/primitives/modal.tsx b/js/components/src/components/ui/primitives/modal.tsx index 70ce6e07..b1fd83e2 100644 --- a/js/components/src/components/ui/primitives/modal.tsx +++ b/js/components/src/components/ui/primitives/modal.tsx @@ -32,8 +32,9 @@ export const ModalRoot = forwardRef( children, onRequestClose, animationType = "fade", - presentationStyle = Platform.OS === "ios" ? "pageSheet" : "fullScreen", - statusBarTranslucent = Platform.OS === "android", + presentationStyle = Platform.OS === "ios" ? "pageSheet" : "formSheet", + statusBarTranslucent = Platform.OS !== "ios", + transparent = true, ...props }, ref, @@ -57,6 +58,7 @@ export const ModalRoot = forwardRef( animationType={animationType} presentationStyle={presentationStyle} statusBarTranslucent={statusBarTranslucent} + transparent={transparent} {...props} > -- 2.51.2 From 3f86fb6f1d3f12f6e351436cdfd4255a4dfdc3d3 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 19:12:49 -0500 Subject: [PATCH 107/525] update dialog to optionally be a scrollview --- js/components/src/components/ui/dialog.tsx | 418 +++++++++++++----- .../src/components/ui/primitives/input.tsx | 21 +- js/components/src/components/ui/textarea.tsx | 76 ++-- 3 files changed, 385 insertions(+), 130 deletions(-) diff --git a/js/components/src/components/ui/dialog.tsx b/js/components/src/components/ui/dialog.tsx index 508d5e4b..7c75c54b 100644 --- a/js/components/src/components/ui/dialog.tsx +++ b/js/components/src/components/ui/dialog.tsx @@ -1,11 +1,21 @@ +import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet"; +// to get the portal +import * as Portal from "@rn-primitives/portal"; import { cva, type VariantProps } from "class-variance-authority"; import { X } from "lucide-react-native"; -import React, { forwardRef } from "react"; -import { Platform, Text } from "react-native"; +import React, { forwardRef, useRef } from "react"; +import { + Platform, + Pressable, + StyleSheet, + useWindowDimensions, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useTheme } from "../../lib/theme/theme"; -import * as zero from "../../ui"; import { createThemedIcon } from "./icons"; import { ModalPrimitive, ModalPrimitiveProps } from "./primitives/modal"; +import { Text } from "./text"; const ThemedX = createThemedIcon(X); @@ -50,10 +60,138 @@ export interface DialogProps onClose?: () => void; } +// Bottom Sheet Dialog Component +const DialogBottomSheet = forwardRef< + any, + DialogProps & { + overlayStyle?: any; + portalHost?: string; + } +>(function DialogBottomSheet( + { + overlayStyle, + portalHost, + children, + title, + description, + showCloseButton = true, + onClose, + open = false, + onOpenChange, + ...props + }, + _ref, +) { + const { theme } = useTheme(); + const sheetRef = useRef(null); + const { top } = useSafeAreaInsets(); + const dims = useWindowDimensions(); + + const handleClose = React.useCallback(() => { + if (onClose) { + onClose(); + } + if (onOpenChange) { + onOpenChange(false); + } + }, [onClose, onOpenChange]); + + if (!open) { + return null; + } + + return ( + + ( + + )} + onClose={handleClose} + style={[overlayStyle]} + backgroundStyle={{ + backgroundColor: theme.colors.card, + borderRadius: theme.borderRadius.lg, + ...theme.shadows.lg, + }} + handleIndicatorStyle={{ + width: 48, + height: 4, + backgroundColor: theme.colors.textMuted, + }} + > + + {/* Header */} + {(title || showCloseButton) && ( + + {title && {title}} + {showCloseButton && ( + + + + )} + + )} + + {/* Scrollable Content */} + + {description && ( + {description} + )} + {children} + + + + + ); +}); + export const Dialog = forwardRef( ( { - variant = "left", + variant = "default", size = "md", position = "center", children, @@ -68,70 +206,10 @@ export const Dialog = forwardRef( }, ref, ) => { - const { zero: zt, theme } = useTheme(); - - // Content styles using theme.zero - const contentStyles = React.useMemo(() => { - const baseStyle = [ - zt.bg.card, - zero.r.lg, - zero.shadows.lg, - { maxHeight: "90%", maxWidth: "90%" }, - ]; - - const variantStyle = (() => { - switch (variant) { - case "sheet": - return [ - { borderRadius: zero.borderRadius.xl }, - { - borderBottomLeftRadius: 0, - borderBottomRightRadius: 0, - marginTop: "auto", - marginBottom: 0, - maxHeight: "80%", - width: "100%", - maxWidth: "100%", - }, - ]; - case "fullscreen": - return [ - { - width: "100%", - height: "100%", - maxWidth: "100%", - maxHeight: "100%", - borderRadius: 0, - margin: 0, - }, - ]; - default: - return []; - } - })(); - - const sizeStyle = (() => { - switch (size) { - case "sm": - return { minWidth: 300, minHeight: 200 }; - case "lg": - return { minWidth: 500, minHeight: 400 }; - case "xl": - return { minWidth: 600, minHeight: 500 }; - case "full": - return { - width: "95%", - height: "95%", - maxWidth: "95%", - maxHeight: "95%", - }; - default: - return { minWidth: 400, minHeight: 300 }; - } - })(); - - return [baseStyle, variantStyle, sizeStyle].flat(); - }, [variant, size, zero]); + const { theme } = useTheme(); + + // Create dynamic styles based on theme + const styles = React.useMemo(() => createStyles(theme), [theme]); const handleClose = React.useCallback(() => { if (onClose) { @@ -173,38 +251,32 @@ export const Dialog = forwardRef( {(title || showCloseButton) && ( {title} {showCloseButton && ( @@ -214,7 +286,7 @@ export const Dialog = forwardRef( {description && ( {description} @@ -230,6 +302,43 @@ export const Dialog = forwardRef( Dialog.displayName = "Dialog"; +/// Responsive Dialog Component. On mobile this will render a *bottom sheet*. +/// Prefer this over the regular Dialog component for better mobile UX. +export const ResponsiveDialog = forwardRef( + ({ children, size, ...props }, ref) => { + const { width } = useWindowDimensions(); + + // On web, you might want to always use the normal dialog + // On mobile (width < 800), use the bottom sheet + const isBottomSheet = Platform.OS !== "web" && width < 800; + + if (isBottomSheet) { + return ( + + {children} + + ); + } + + // Use larger default size for regular dialogs to give more room + const dialogSize = size || "lg"; + + return ( + + {children} + + ); + }, +); + +ResponsiveDialog.displayName = "ResponsiveDialog"; + // Dialog Title component export interface DialogTitleProps { children?: React.ReactNode; @@ -238,16 +347,13 @@ export interface DialogTitleProps { export const DialogTitle = forwardRef( ({ children, style, ...props }, ref) => { - const { zero: zt } = useTheme(); + const { theme } = useTheme(); + const styles = React.useMemo(() => createStyles(theme), [theme]); if (!children) return null; return ( - + {children} ); @@ -264,12 +370,13 @@ export interface DialogDescriptionProps { export const DialogDescription = forwardRef( ({ children, style, ...props }, ref) => { - const { zero: zt } = useTheme(); + const { theme } = useTheme(); + const styles = React.useMemo(() => createStyles(theme), [theme]); if (!children) return null; return ( - + {children} ); @@ -304,7 +411,8 @@ export const DialogFooter = forwardRef( }, ref, ) => { - const { zero: zt } = useTheme(); + const { theme } = useTheme(); + const styles = React.useMemo(() => createStyles(theme), [theme]); if (!children) return null; @@ -314,7 +422,7 @@ export const DialogFooter = forwardRef( withBorder={withBorder} direction={direction} justify={justify} - style={[zero.p[6], { gap: 8 }, style]} + style={[styles.footer, style]} {...props} > {children} @@ -330,5 +438,117 @@ const DialogCloseIcon = () => { return ; }; +// Create theme-aware styles +function createStyles(theme: any) { + return StyleSheet.create({ + overlay: { + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + + content: { + backgroundColor: theme.colors.card, + borderRadius: theme.borderRadius.lg, + ...theme.shadows.lg, + maxHeight: "90%", + maxWidth: "90%", + }, + + // Variant styles + sheetContent: { + borderTopLeftRadius: theme.borderRadius.xl, + borderTopRightRadius: theme.borderRadius.xl, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + marginTop: "auto", + marginBottom: 0, + maxHeight: "80%", + width: "100%", + maxWidth: "100%", + }, + + fullscreenContent: { + width: "100%", + height: "100%", + maxWidth: "100%", + maxHeight: "100%", + borderRadius: 0, + margin: 0, + }, + + // Size styles + smContent: { + minWidth: 300, + minHeight: 200, + }, + + mdContent: { + minWidth: 400, + minHeight: 300, + }, + + lgContent: { + minWidth: 500, + minHeight: 400, + }, + + xlContent: { + minWidth: 600, + minHeight: 500, + }, + + fullContent: { + width: "95%", + height: "95%", + maxWidth: "95%", + maxHeight: "95%", + }, + + header: { + paddingHorizontal: theme.spacing[6], + paddingVertical: theme.spacing[4], + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + + body: { + paddingHorizontal: theme.spacing[6], + paddingBottom: theme.spacing[6], + flex: 1, + }, + + footer: { + paddingHorizontal: theme.spacing[6], + paddingVertical: theme.spacing[4], + gap: theme.spacing[2], + width: "100%", + }, + + title: { + fontSize: 20, + fontWeight: "600", + color: theme.colors.text, + flex: 1, + lineHeight: 24, + }, + + description: { + fontSize: 16, + color: theme.colors.textMuted, + lineHeight: 22, + marginVertical: theme.spacing[4], + }, + + closeButton: { + width: theme.touchTargets.minimum, + height: theme.touchTargets.minimum, + alignItems: "center", + justifyContent: "center", + borderRadius: theme.borderRadius.sm, + marginLeft: theme.spacing[2], + }, + }); +} + // Export dialog variants for external use export { dialogVariants }; diff --git a/js/components/src/components/ui/primitives/input.tsx b/js/components/src/components/ui/primitives/input.tsx index d54340ed..8d8db71d 100644 --- a/js/components/src/components/ui/primitives/input.tsx +++ b/js/components/src/components/ui/primitives/input.tsx @@ -1,3 +1,7 @@ +import { + BottomSheetTextInput, + useBottomSheetInternal, +} from "@gorhom/bottom-sheet"; import React, { forwardRef } from "react"; import { NativeSyntheticEvent, @@ -24,7 +28,7 @@ export interface InputPrimitiveProps extends Omit { } // Input root primitive - the main TextInput component -export const InputRoot = forwardRef( +export const InputRoot = forwardRef( ( { value, @@ -44,6 +48,19 @@ export const InputRoot = forwardRef( ) => { const [isFocused, setIsFocused] = React.useState(false); + let isInBottomSheet = false; + try { + useBottomSheetInternal(); + isInBottomSheet = true; + } catch { + isInBottomSheet = false; + } + + const InputComponent = + isInBottomSheet && Platform.OS !== "web" + ? BottomSheetTextInput + : TextInput; + const handleChangeText = React.useCallback( (text: string) => { if (onChangeText) { @@ -77,7 +94,7 @@ export const InputRoot = forwardRef( ); return ( - ; -}) { - return ( - - ); -} +const Textarea = React.forwardRef( + ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => { + // Detect if we're inside a bottom sheet + let isInBottomSheet = false; + try { + useBottomSheetInternal(); + isInBottomSheet = true; + } catch { + // Not in a bottom sheet context + isInBottomSheet = false; + } + + // Use BottomSheetTextInput when inside a bottom sheet, regular TextInput otherwise + const InputComponent = + isInBottomSheet && Platform.OS !== "web" + ? BottomSheetTextInput + : TextInput; + + return ( + + ); + }, +); + +Textarea.displayName = "Textarea"; export { Textarea }; -- 2.51.2 From 90fa9ac6c7de2a9f1d4db0ec39f01ba4cc4f4773 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 19:15:56 -0500 Subject: [PATCH 108/525] update stuff to support responsive dialogs --- .../components/settings/webhook-manager.tsx | 5 +++-- .../mobile-player/ui/report-modal.tsx | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/js/app/components/settings/webhook-manager.tsx b/js/app/components/settings/webhook-manager.tsx index 890163b9..85ecba8c 100644 --- a/js/app/components/settings/webhook-manager.tsx +++ b/js/app/components/settings/webhook-manager.tsx @@ -3,6 +3,7 @@ import { Dialog, DialogFooter, Input, + ResponsiveDialog, Text, zero, } from "@streamplace/components"; @@ -340,7 +341,7 @@ function WebhookForm({ }; return ( - !open && onClose()} title={webhook ? "Edit Webhook" : "Create Webhook"} @@ -598,7 +599,7 @@ function WebhookForm({ {isLoading ? "Saving..." : webhook ? "Update" : "Create"} - + ); } diff --git a/js/components/src/components/mobile-player/ui/report-modal.tsx b/js/components/src/components/mobile-player/ui/report-modal.tsx index 387ffe60..096476d0 100644 --- a/js/components/src/components/mobile-player/ui/report-modal.tsx +++ b/js/components/src/components/mobile-player/ui/report-modal.tsx @@ -9,11 +9,11 @@ import { zero } from "../../.."; import { useSubmitReport } from "../../../livestream-store"; import { Button, - Dialog, DialogFooter, - ModalContent, + ResponsiveDialog, Text, Textarea, + useTheme, } from "../../ui"; // AT Protocol moderation reason types with proper labels @@ -72,6 +72,8 @@ export const ReportModal: React.FC = ({ const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const { theme } = useTheme(); + const submitReport = useSubmitReport(); const handleCancel = () => { @@ -107,7 +109,7 @@ export const ReportModal: React.FC = ({ }; return ( - = ({ dismissible={false} position="center" > - + {REPORT_REASONS.map((reason) => ( = ({ ]} > - {selectedReason === reason.value ? : } + {selectedReason === reason.value ? ( + + ) : ( + + )} = ({ )} - + - + ); }; -- 2.51.2 From ae0152f979f0eaafd4abd1f3e03f64049d013359 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 3 Oct 2025 19:29:56 -0500 Subject: [PATCH 109/525] remove funny magic number --- js/components/src/components/ui/dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/src/components/ui/dialog.tsx b/js/components/src/components/ui/dialog.tsx index 7c75c54b..984f83f1 100644 --- a/js/components/src/components/ui/dialog.tsx +++ b/js/components/src/components/ui/dialog.tsx @@ -107,7 +107,7 @@ const DialogBottomSheet = forwardRef< index={open ? 0 : -1} enablePanDownToClose enableDynamicSizing={true} - maxDynamicContentSize={dims.height - top - 100} + maxDynamicContentSize={dims.height - top} keyboardBehavior="interactive" keyboardBlurBehavior="restore" enableContentPanningGesture={false} -- 2.51.2 From f09c80d3256abe72ca4237c4b3416b6575b3d70c Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 18:12:35 -0700 Subject: [PATCH 110/525] iroh: add AddTickets --- .../iroh_streamplace/iroh_streamplace.go | 43 +++++++++++++++++++ .../iroh_streamplace/iroh_streamplace.h | 11 +++++ pkg/replication/iroh_replicator/kv.go | 31 ++++++++----- rust/iroh-streamplace/src/node/streams.rs | 41 ++++++++++++++++++ 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go index d2b1feca..2e3ddfbb 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.go @@ -496,6 +496,15 @@ func uniffiCheckChecksums() { panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_gosigner_sign: UniFFI API checksum mismatch") } } + { + checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_iroh_streamplace_checksum_method_node_add_tickets() + }) + if checksum != 8701 { + // If this happens try cleaning and rebuilding your project + panic("iroh_streamplace: uniffi_iroh_streamplace_checksum_method_node_add_tickets: UniFFI API checksum mismatch") + } + } { checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_iroh_streamplace_checksum_method_node_db() @@ -1568,6 +1577,8 @@ func (c FfiConverterGoSigner) register() { // Iroh-streamplace node that can send, forward or receive stream segments. type NodeInterface interface { + // Add tickets for remote peers + AddTickets(peers []string) error // Get a handle to the db to watch for changes locally or globally. Db() *Db // Join peers by their node tickets. @@ -1684,6 +1695,38 @@ func NodeSender(config Config) (*Node, error) { return res, err } +// Add tickets for remote peers +func (_self *Node) AddTickets(peers []string) error { + _pointer := _self.ffiObject.incrementPointer("*Node") + defer _self.ffiObject.decrementPointer() + _, err := uniffiRustCallAsync[JoinPeersError]( + FfiConverterJoinPeersErrorINSTANCE, + // completeFn + func(handle C.uint64_t, status *C.RustCallStatus) struct{} { + C.ffi_iroh_streamplace_rust_future_complete_void(handle, status) + return struct{}{} + }, + // liftFn + func(_ struct{}) struct{} { return struct{}{} }, + C.uniffi_iroh_streamplace_fn_method_node_add_tickets( + _pointer, FfiConverterSequenceStringINSTANCE.Lower(peers)), + // pollFn + func(handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_poll_void(handle, continuation, data) + }, + // freeFn + func(handle C.uint64_t) { + C.ffi_iroh_streamplace_rust_future_free_void(handle) + }, + ) + + if err == nil { + return nil + } + + return err +} + // Get a handle to the db to watch for changes locally or globally. func (_self *Node) Db() *Db { _pointer := _self.ffiObject.incrementPointer("*Node") diff --git a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h index 7f735d8a..f7d04107 100644 --- a/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h +++ b/pkg/iroh/generated/iroh_streamplace/iroh_streamplace.h @@ -568,6 +568,11 @@ uint64_t uniffi_iroh_streamplace_fn_constructor_node_receiver(RustBuffer config, uint64_t uniffi_iroh_streamplace_fn_constructor_node_sender(RustBuffer config ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_ADD_TICKETS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_ADD_TICKETS +uint64_t uniffi_iroh_streamplace_fn_method_node_add_tickets(void* ptr, RustBuffer peers +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_DB #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_FN_METHOD_NODE_DB void* uniffi_iroh_streamplace_fn_method_node_db(void* ptr, RustCallStatus *out_status @@ -1107,6 +1112,12 @@ uint16_t uniffi_iroh_streamplace_checksum_method_filter_timestamps(void #define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_GOSIGNER_SIGN uint16_t uniffi_iroh_streamplace_checksum_method_gosigner_sign(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_ADD_TICKETS +#define UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_ADD_TICKETS +uint16_t uniffi_iroh_streamplace_checksum_method_node_add_tickets(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IROH_STREAMPLACE_CHECKSUM_METHOD_NODE_DB diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go index 639cb544..d1b6d3e9 100644 --- a/pkg/replication/iroh_replicator/kv.go +++ b/pkg/replication/iroh_replicator/kv.go @@ -15,13 +15,14 @@ import ( ) type IrohSwarm struct { - Node *iroh_streamplace.Node - DB *iroh_streamplace.Db - w *iroh_streamplace.WriteScope - mm *media.MediaManager - segChan chan *media.NewSegmentNotification - nodeId string - activeSubs map[string]*OriginInfo + Node *iroh_streamplace.Node + DB *iroh_streamplace.Db + w *iroh_streamplace.WriteScope + mm *media.MediaManager + segChan chan *media.NewSegmentNotification + nodeId string + activeSubs map[string]*OriginInfo + handleDataScoped func(topic string, data []byte) } // A message saying "hey I ingested node data at this time" @@ -46,6 +47,17 @@ func NewSwarm(ctx context.Context, tickets []string, secret []byte, mm *media.Me activeSubs: make(map[string]*OriginInfo), } + // workaround to get context into the HandleData callback + swarm.handleDataScoped = func(topic string, data []byte) { + if ctx.Err() != nil { + return + } + err := swarm.mm.ValidateMP4(context.Background(), bytes.NewReader(data), false) + if err != nil { + log.Error(ctx, "could not validate segment", "error", err, "topic", topic, "data", len(data)) + } + } + node, err := iroh_streamplace.NodeReceiver(config, &swarm) if err != nil { return nil, fmt.Errorf("failed to create NodeSender: %w", err) @@ -193,10 +205,7 @@ func (swarm *IrohSwarm) startSegmentSender(ctx context.Context) error { } func (swarm *IrohSwarm) HandleData(topic string, data []byte) { - err := swarm.mm.ValidateMP4(context.Background(), bytes.NewReader(data), false) - if err != nil { - log.Error(context.Background(), "could not validate segment", "error", err, "topic", topic, "data", len(data)) - } + swarm.handleDataScoped(topic, data) } func (swarm *IrohSwarm) SendSegment(ctx context.Context, not *media.NewSegmentNotification) error { diff --git a/rust/iroh-streamplace/src/node/streams.rs b/rust/iroh-streamplace/src/node/streams.rs index 0e3b8d8b..abd696a7 100644 --- a/rust/iroh-streamplace/src/node/streams.rs +++ b/rust/iroh-streamplace/src/node/streams.rs @@ -97,6 +97,11 @@ mod api { pub peers: Vec, } + #[derive(Debug, Serialize, Deserialize)] + pub struct AddTickets { + pub peers: Vec, + } + #[derive(Debug, Serialize, Deserialize)] pub struct GetNodeAddr; @@ -113,6 +118,8 @@ mod api { SendSegment(SendSegment), #[rpc(tx=oneshot::Sender<()>)] JoinPeers(JoinPeers), + #[rpc(tx=oneshot::Sender<()>)] + AddTickets(AddTickets), #[rpc(tx=oneshot::Sender)] GetNodeAddr(GetNodeAddr), } @@ -477,6 +484,19 @@ impl Actor { self.subscriptions.remove(&key); tx.send(()).await.ok(); } + ApiMessage::AddTickets(msg) => { + trace!("{:?}", msg.inner); + let WithChannels { + tx, + inner: api::AddTickets { peers }, + .. + } = msg; + for addr in &peers { + self.router.endpoint().add_node_addr(addr.clone()).ok(); + } + // self.client.inner().join_peers(ids).await.ok(); + tx.send(()).await.ok(); + } ApiMessage::JoinPeers(msg) => { trace!("{:?}", msg.inner); let WithChannels { @@ -708,6 +728,27 @@ impl Node { }) } + /// Add tickets for remote peers + pub async fn add_tickets(&self, peers: Vec) -> Result<(), JoinPeersError> { + let peers = peers + .iter() + .map(|p| NodeTicket::from_str(p)) + .collect::, _>>() + .map_err(|e| JoinPeersError::Ticket { + message: e.to_string(), + })?; + let addrs = peers + .iter() + .map(|t| t.node_addr().clone()) + .collect::>(); + self.api + .rpc(api::AddTickets { peers: addrs }) + .await + .map_err(|e| JoinPeersError::Irpc { + message: e.to_string(), + }) + } + /// Get this node's ticket. pub async fn ticket(&self) -> Result { let addr = self -- 2.51.2 From 1d405387463bafa9c149feadc50f0d81c71c4d32 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Fri, 3 Oct 2025 18:37:03 -0700 Subject: [PATCH 111/525] docker: bump golangci-lint --- .ci/dockerfile-hash.yaml | 2 +- docker/build.Dockerfile | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/dockerfile-hash.yaml b/.ci/dockerfile-hash.yaml index 160304ab..18b503a0 100644 --- a/.ci/dockerfile-hash.yaml +++ b/.ci/dockerfile-hash.yaml @@ -1,2 +1,2 @@ variables: - DOCKERFILE_HASH: a0cc55e6b9dc095726690e60bec764a94031d610 \ No newline at end of file + DOCKERFILE_HASH: 8ba2805e78d8d14ab757ef54f89a9778c925d6ad \ No newline at end of file diff --git a/docker/build.Dockerfile b/docker/build.Dockerfile index a0cc55e6..8ba2805e 100644 --- a/docker/build.Dockerfile +++ b/docker/build.Dockerfile @@ -89,10 +89,10 @@ RUN go env -w GOTOOLCHAIN=go$GO_VERSION # && cd .. \ # && rm -rf streamplace -RUN curl -L https://github.com/golangci/golangci-lint/releases/download/v2.1.6/golangci-lint-2.1.6-linux-amd64.tar.gz -o golangci-lint.tar.gz \ +RUN curl -L https://github.com/golangci/golangci-lint/releases/download/v2.5.0/golangci-lint-2.5.0-linux-amd64.tar.gz -o golangci-lint.tar.gz \ && tar -xf golangci-lint.tar.gz \ - && mv golangci-lint-2.1.6-linux-amd64/golangci-lint /usr/local/bin/ \ - && rm -rf golangci-lint.tar.gz golangci-lint-2.1.6-linux-amd64 + && mv golangci-lint-2.5.0-linux-amd64/golangci-lint /usr/local/bin/ \ + && rm -rf golangci-lint.tar.gz golangci-lint-2.5.0-linux-amd64 RUN gem install fpm ENV APTLY_VERSION 1.6.2 -- 2.51.2 From 113d529bf5b741af4dc4cbe3df3e38016876e6f9 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Sat, 4 Oct 2025 00:04:58 -0500 Subject: [PATCH 112/525] upsert livestrem record --- pkg/model/livestream.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/model/livestream.go b/pkg/model/livestream.go index 490d12e9..91da486b 100644 --- a/pkg/model/livestream.go +++ b/pkg/model/livestream.go @@ -9,6 +9,7 @@ import ( "github.com/bluesky-social/indigo/api/bsky" lexutil "github.com/bluesky-social/indigo/lex/util" "gorm.io/gorm" + "gorm.io/gorm/clause" "stream.place/streamplace/pkg/streamplace" ) @@ -44,7 +45,11 @@ func (ls *Livestream) ToLivestreamView() (*streamplace.Livestream_LivestreamView } func (m *DBModel) CreateLivestream(ctx context.Context, ls *Livestream) error { - return m.DB.Create(ls).Error + // upsert livestream record, actually + return m.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "uri"}}, + DoUpdates: clause.AssignmentColumns([]string{"cid", "created_at", "livestream", "repo_did", "post_cid", "post_uri"}), + }).Create(ls).Error } // GetLatestLivestreamForRepo returns the most recent livestream for a given repo DID -- 2.51.2 From e284654ce049c5fec497042c1312903c7ecc2637 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 4 Oct 2025 13:14:31 -0700 Subject: [PATCH 113/525] lexicons: add place.stream.broadcast stuff --- .../place-stream-broadcast-origin.md | 72 +++ .../place-stream-broadcast-syndication.md | 66 +++ lexicons/place/stream/broadcast/origin.json | 37 ++ .../place/stream/broadcast/syndication.json | 32 ++ pkg/api/feeds.go | 4 +- pkg/atproto/chat_message_test.go | 8 +- pkg/atproto/handle_test.go | 8 +- pkg/atproto/lexicon_repo_test.go | 4 +- pkg/cmd/streamplace.go | 8 +- pkg/config/config.go | 19 +- pkg/director/director.go | 6 +- pkg/director/stream_session.go | 6 +- pkg/gen/gen.go | 2 + pkg/media/peer_connection.go | 2 +- pkg/replication/iroh_replicator/kv.go | 11 +- pkg/spxrpc/com_atproto_moderation.go | 2 +- pkg/spxrpc/com_atproto_repo.go | 2 +- pkg/spxrpc/com_atproto_server.go | 4 +- pkg/streamplace/broadcastorigin.go | 25 + pkg/streamplace/broadcastsyndication.go | 23 + pkg/streamplace/cbor_gen.go | 454 ++++++++++++++++++ 21 files changed, 763 insertions(+), 32 deletions(-) create mode 100644 js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-origin.md create mode 100644 js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-syndication.md create mode 100644 lexicons/place/stream/broadcast/origin.json create mode 100644 lexicons/place/stream/broadcast/syndication.json create mode 100644 pkg/streamplace/broadcastorigin.go create mode 100644 pkg/streamplace/broadcastsyndication.go diff --git a/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-origin.md b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-origin.md new file mode 100644 index 00000000..8727456a --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-origin.md @@ -0,0 +1,72 @@ +--- +title: place.stream.broadcast.origin +description: Reference for the place.stream.broadcast.origin lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `record` + +Record indicating a livestream is published and available for replication at a +given address. By convention, the record key is streamer::server + +**Record Key:** `any` + +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ------------ | -------- | ----- | --------------------------------------------------------------------- | ------------------ | +| `streamer` | `string` | ✅ | DID of the streamer whose livestream is being published | Format: `did` | +| `server` | `string` | ✅ | did:web of the server that's currently rebroadcasting the livestream | Format: `did` | +| `updatedAt` | `string` | ✅ | Periodically updated timestamp when this origin last saw a livestream | Format: `datetime` | +| `irohTicket` | `string` | ❌ | Iroh ticket that can be used to access the livestream from the server | Max Length: 2048 | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.broadcast.origin", + "defs": { + "main": { + "type": "record", + "key": "any", + "description": "Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server", + "record": { + "type": "object", + "required": ["streamer", "server", "updatedAt"], + "properties": { + "streamer": { + "type": "string", + "format": "did", + "description": "DID of the streamer whose livestream is being published" + }, + "server": { + "type": "string", + "format": "did", + "description": "did:web of the server that's currently rebroadcasting the livestream" + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Periodically updated timestamp when this origin last saw a livestream" + }, + "irohTicket": { + "type": "string", + "maxLength": 2048, + "description": "Iroh ticket that can be used to access the livestream from the server" + } + } + } + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-syndication.md b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-syndication.md new file mode 100644 index 00000000..16366992 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/broadcast/place-stream-broadcast-syndication.md @@ -0,0 +1,66 @@ +--- +title: place.stream.broadcast.syndication +description: Reference for the place.stream.broadcast.syndication lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `record` + +Record created by a Streamplace broadcaster to indicate that they will be +replicating a livestream + +**Record Key:** `tid` + +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ------------- | -------- | ----- | -------------------------------------------------------------------------- | ------------------ | +| `broadcaster` | `string` | ✅ | DID of the Streamplace broadcaster that will be replicating the livestream | Format: `did` | +| `streamer` | `string` | ✅ | DID of the streamer whose livestream is being replicated | Format: `did` | +| `createdAt` | `string` | ✅ | Client-declared timestamp when this syndication was created. | Format: `datetime` | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.broadcast.syndication", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream", + "record": { + "type": "object", + "required": ["broadcaster", "streamer", "createdAt"], + "properties": { + "broadcaster": { + "type": "string", + "format": "did", + "description": "DID of the Streamplace broadcaster that will be replicating the livestream" + }, + "streamer": { + "type": "string", + "format": "did", + "description": "DID of the streamer whose livestream is being replicated" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this syndication was created." + } + } + } + } + } +} +``` diff --git a/lexicons/place/stream/broadcast/origin.json b/lexicons/place/stream/broadcast/origin.json new file mode 100644 index 00000000..b31d7eb9 --- /dev/null +++ b/lexicons/place/stream/broadcast/origin.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "place.stream.broadcast.origin", + "defs": { + "main": { + "type": "record", + "key": "any", + "description": "Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server", + "record": { + "type": "object", + "required": ["streamer", "server", "updatedAt"], + "properties": { + "streamer": { + "type": "string", + "format": "did", + "description": "DID of the streamer whose livestream is being published" + }, + "server": { + "type": "string", + "format": "did", + "description": "did:web of the server that's currently rebroadcasting the livestream" + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "Periodically updated timestamp when this origin last saw a livestream" + }, + "irohTicket": { + "type": "string", + "maxLength": 2048, + "description": "Iroh ticket that can be used to access the livestream from the server" + } + } + } + } + } +} diff --git a/lexicons/place/stream/broadcast/syndication.json b/lexicons/place/stream/broadcast/syndication.json new file mode 100644 index 00000000..84342ac5 --- /dev/null +++ b/lexicons/place/stream/broadcast/syndication.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "place.stream.broadcast.syndication", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream", + "record": { + "type": "object", + "required": ["broadcaster", "streamer", "createdAt"], + "properties": { + "broadcaster": { + "type": "string", + "format": "did", + "description": "DID of the Streamplace broadcaster that will be replicating the livestream" + }, + "streamer": { + "type": "string", + "format": "did", + "description": "DID of the streamer whose livestream is being replicated" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this syndication was created." + } + } + } + } + } +} diff --git a/pkg/api/feeds.go b/pkg/api/feeds.go index 11b5771f..8b1980eb 100644 --- a/pkg/api/feeds.go +++ b/pkg/api/feeds.go @@ -13,7 +13,7 @@ import ( func (a *StreamplaceAPI) HandleDidJSON(ctx context.Context) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - host := a.CLI.PublicHost + host := a.CLI.BroadcasterHost didJSON := atproto.DIDDoc(host) w.WriteHeader(200) w.Header().Set("Content-Type", "application/json") @@ -32,7 +32,7 @@ func (a *StreamplaceAPI) HandleAtprotoDID(ctx context.Context) httprouter.Handle return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { did := a.CLI.AtprotoDID if did == "" { - did = fmt.Sprintf("did:web:%s", a.CLI.PublicHost) + did = fmt.Sprintf("did:web:%s", a.CLI.BroadcasterHost) } _, err := fmt.Fprintf(w, "%s", did) if err != nil { diff --git a/pkg/atproto/chat_message_test.go b/pkg/atproto/chat_message_test.go index b836a871..7042e784 100644 --- a/pkg/atproto/chat_message_test.go +++ b/pkg/atproto/chat_message_test.go @@ -25,10 +25,10 @@ func TestChatMessage(t *testing.T) { dev := devenv.WithDevEnv(t) t.Logf("dev: %+v", dev) cli := config.CLI{ - PublicHost: "example.com", - DBURL: ":memory:", - RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), - PLCURL: dev.PLCURL, + BroadcasterHost: "example.com", + DBURL: ":memory:", + RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), + PLCURL: dev.PLCURL, } t.Logf("cli: %+v", cli) b := bus.NewBus() diff --git a/pkg/atproto/handle_test.go b/pkg/atproto/handle_test.go index 4ac391a8..01ca7439 100644 --- a/pkg/atproto/handle_test.go +++ b/pkg/atproto/handle_test.go @@ -27,10 +27,10 @@ func TestHandleChange(t *testing.T) { dev := devenv.WithDevEnv(t) t.Logf("dev: %+v", dev) cli := config.CLI{ - PublicHost: "example.com", - DBURL: ":memory:", - RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), - PLCURL: dev.PLCURL, + BroadcasterHost: "example.com", + DBURL: ":memory:", + RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), + PLCURL: dev.PLCURL, } t.Logf("cli: %+v", cli) diff --git a/pkg/atproto/lexicon_repo_test.go b/pkg/atproto/lexicon_repo_test.go index 364b60d6..a70f6c09 100644 --- a/pkg/atproto/lexicon_repo_test.go +++ b/pkg/atproto/lexicon_repo_test.go @@ -16,8 +16,8 @@ import ( func TestLexiconRepo(t *testing.T) { cli := config.CLI{ - PublicHost: "example.com", - DBURL: ":memory:", + BroadcasterHost: "example.com", + DBURL: ":memory:", } cli.DataDir = t.TempDir() mod, err := model.MakeDB(":memory:") diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 2c26758c..bc997e16 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -374,8 +374,8 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { Scope: "atproto transition:generic", ClientName: "Streamplace", RedirectURIs: []string{ - fmt.Sprintf("https://%s/login", cli.PublicHost), - fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), + fmt.Sprintf("https://%s/login", cli.BroadcasterHost), + fmt.Sprintf("https://%s/api/app-return", cli.BroadcasterHost), }, } @@ -406,7 +406,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { } op := oatproxy.New(&oatproxy.Config{ - Host: cli.PublicHost, + Host: cli.BroadcasterHost, CreateOAuthSession: state.CreateOAuthSession, UpdateOAuthSession: state.UpdateOAuthSession, GetOAuthSession: state.LoadOAuthSession, @@ -416,7 +416,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { DownstreamJWK: cli.AccessJWK, ClientMetadata: clientMetadata, }) - d := director.NewDirector(mm, mod, &cli, b, op, state) + d := director.NewDirector(mm, mod, &cli, b, op, state, swarm) a, err := api.MakeStreamplaceAPI(&cli, mod, state, eip712signer, noter, mm, ms, b, atsync, d, op) if err != nil { return err diff --git a/pkg/config/config.go b/pkg/config/config.go index f8743cbf..f8aa3e7f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "context" "crypto/rsa" "crypto/x509" "encoding/json" @@ -28,6 +29,7 @@ import ( "stream.place/streamplace/pkg/constants" "stream.place/streamplace/pkg/crypto/aqpub" "stream.place/streamplace/pkg/integrations/discord/discordtypes" + "stream.place/streamplace/pkg/log" ) const SPDataDir = "$SP_DATA_DIR" @@ -100,7 +102,9 @@ type CLI struct { ExternalSigning bool RTMPServerAddon string TracingEndpoint string - PublicHost string + BroadcasterHost string + XXDeprecatedPublicHost string + ServerHost string RateLimitPerSecond int RateLimitBurst int RateLimitWebsocket int @@ -164,7 +168,9 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { fs.StringVar(&cli.RelayHost, "relay-host", "wss://bsky.network", "websocket url for relay firehose") fs.Bool("insecure", false, "DEPRECATED, does nothing.") fs.StringVar(&cli.Color, "color", "", "'true' to enable colorized logging, 'false' to disable") - fs.StringVar(&cli.PublicHost, "public-host", "", "public host for this streamplace node (excluding https:// e.g. stream.place)") + fs.StringVar(&cli.BroadcasterHost, "broadcaster-host", "", "public host for the broadcaster group that this node is a part of (excluding https:// e.g. stream.place)") + fs.StringVar(&cli.XXDeprecatedPublicHost, "public-host", "", "deprecated, use broadcaster-host or server-host instead as appropriate") + fs.StringVar(&cli.ServerHost, "server-host", "", "public host for this particular physical streamplace node. defaults to broadcaster-host and only must be set for multi-node broadcasters") fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation") fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps") fs.BoolVar(&cli.ExternalSigning, "external-signing", false, "enable external signing via exec (prevents potential memory leak)") @@ -311,6 +317,13 @@ func (cli *CLI) Parse(fs *flag.FlagSet, args []string) error { })), ) } + if cli.XXDeprecatedPublicHost != "" && cli.BroadcasterHost == "" { + log.Warn(context.Background(), "public-host is deprecated, use broadcaster-host or server-host instead as appropriate") + cli.BroadcasterHost = cli.XXDeprecatedPublicHost + } + if cli.ServerHost == "" && cli.BroadcasterHost != "" { + cli.ServerHost = cli.BroadcasterHost + } return nil } @@ -528,5 +541,5 @@ func (cli *CLI) StreamIsAllowed(did string) error { } func (cli *CLI) MyDID() string { - return fmt.Sprintf("did:web:%s", cli.PublicHost) + return fmt.Sprintf("did:web:%s", cli.BroadcasterHost) } diff --git a/pkg/director/director.go b/pkg/director/director.go index d708bcb4..1dcce845 100644 --- a/pkg/director/director.go +++ b/pkg/director/director.go @@ -12,6 +12,7 @@ import ( "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/model" + "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/statedb" ) @@ -30,9 +31,10 @@ type Director struct { streamSessionsMu sync.Mutex op *oatproxy.OATProxy statefulDB *statedb.StatefulDB + swarm *iroh_replicator.IrohSwarm } -func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB) *Director { +func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus, op *oatproxy.OATProxy, statefulDB *statedb.StatefulDB, swarm *iroh_replicator.IrohSwarm) *Director { return &Director{ mm: mm, mod: mod, @@ -42,6 +44,7 @@ func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus * streamSessionsMu: sync.Mutex{}, op: op, statefulDB: statefulDB, + swarm: swarm, } } @@ -72,6 +75,7 @@ func (d *Director) Start(ctx context.Context) error { packets: make([]bus.PacketizedSegment, 0), started: make(chan struct{}), statefulDB: d.statefulDB, + swarm: d.swarm, } d.streamSessions[not.Segment.RepoDID] = ss g.Go(func() error { diff --git a/pkg/director/stream_session.go b/pkg/director/stream_session.go index 353cf0db..b4baa49a 100644 --- a/pkg/director/stream_session.go +++ b/pkg/director/stream_session.go @@ -21,6 +21,7 @@ import ( "stream.place/streamplace/pkg/media" "stream.place/streamplace/pkg/model" "stream.place/streamplace/pkg/renditions" + "stream.place/streamplace/pkg/replication/iroh_replicator" "stream.place/streamplace/pkg/spmetrics" "stream.place/streamplace/pkg/statedb" "stream.place/streamplace/pkg/streamplace" @@ -44,6 +45,7 @@ type StreamSession struct { ctx context.Context packets []bus.PacketizedSegment statefulDB *statedb.StatefulDB + swarm *iroh_replicator.IrohSwarm } func (ss *StreamSession) Start(ctx context.Context, not *media.NewSegmentNotification) error { @@ -302,7 +304,7 @@ func (ss *StreamSession) UpdateStatus(ctx context.Context, repoDID string) error return fmt.Errorf("livestream is not a streamplace livestream") } - canonicalUrl := fmt.Sprintf("https://%s/%s", ss.cli.PublicHost, repo.Handle) + canonicalUrl := fmt.Sprintf("https://%s/%s", ss.cli.BroadcasterHost, repo.Handle) if lsr.CanonicalUrl != nil { canonicalUrl = *lsr.CanonicalUrl @@ -313,7 +315,7 @@ func (ss *StreamSession) UpdateStatus(ctx context.Context, repoDID string) error External: &bsky.EmbedExternal_External{ Title: lsr.Title, Uri: canonicalUrl, - Description: fmt.Sprintf("@%s is 🔴LIVE on %s", repo.Handle, ss.cli.PublicHost), + Description: fmt.Sprintf("@%s is 🔴LIVE on %s", repo.Handle, ss.cli.BroadcasterHost), Thumb: thumb, }, }, diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go index 9ba9402d..43baa119 100644 --- a/pkg/gen/gen.go +++ b/pkg/gen/gen.go @@ -25,6 +25,8 @@ func main() { streamplace.ChatMessage_ReplyRef{}, streamplace.ServerSettings{}, streamplace.ChatGate{}, + streamplace.BroadcastOrigin{}, + streamplace.BroadcastSyndication{}, ); err != nil { panic(err) } diff --git a/pkg/media/peer_connection.go b/pkg/media/peer_connection.go index 35b2dab3..6b2b3fef 100644 --- a/pkg/media/peer_connection.go +++ b/pkg/media/peer_connection.go @@ -9,7 +9,7 @@ import ( func (mm *MediaManager) NewPeerConnection(ctx context.Context, user string) (rtcrec.PeerConnection, error) { shouldRecord := false - settings, err := mm.model.GetServerSettings(ctx, mm.cli.PublicHost, user) + settings, err := mm.model.GetServerSettings(ctx, mm.cli.BroadcasterHost, user) if err != nil { return nil, err } diff --git a/pkg/replication/iroh_replicator/kv.go b/pkg/replication/iroh_replicator/kv.go index d1b6d3e9..725cea31 100644 --- a/pkg/replication/iroh_replicator/kv.go +++ b/pkg/replication/iroh_replicator/kv.go @@ -20,7 +20,8 @@ type IrohSwarm struct { w *iroh_streamplace.WriteScope mm *media.MediaManager segChan chan *media.NewSegmentNotification - nodeId string + NodeID string + NodeTicket string activeSubs map[string]*OriginInfo handleDataScoped func(topic string, data []byte) } @@ -75,13 +76,13 @@ func NewSwarm(ctx context.Context, tickets []string, secret []byte, mm *media.Me return nil, fmt.Errorf("failed to get NodeId: %w", err) } log.Log(ctx, "Node ID:", "node_id", nodeId) - swarm.nodeId = nodeId.String() + swarm.NodeID = nodeId.String() ticket, err := node.Ticket() if err != nil { return nil, fmt.Errorf("failed to get Ticket: %w", err) } - log.Log(ctx, "Ticket:", "ticket", ticket) + swarm.NodeTicket = ticket return &swarm, nil } @@ -161,7 +162,7 @@ func (swarm *IrohSwarm) startKV(ctx context.Context) error { } delete(swarm.activeSubs, keyStr) } - if info.NodeID == swarm.nodeId { + if info.NodeID == swarm.NodeID { // oh, i have this stream. cool. do nothing. continue } @@ -213,7 +214,7 @@ func (swarm *IrohSwarm) SendSegment(ctx context.Context, not *media.NewSegmentNo return nil } originInfo := OriginInfo{ - NodeID: swarm.nodeId, + NodeID: swarm.NodeID, Time: not.Segment.StartTime.Format(util.ISO8601), } bs, err := json.Marshal(originInfo) diff --git a/pkg/spxrpc/com_atproto_moderation.go b/pkg/spxrpc/com_atproto_moderation.go index a47d1af9..f1cab7c7 100644 --- a/pkg/spxrpc/com_atproto_moderation.go +++ b/pkg/spxrpc/com_atproto_moderation.go @@ -81,7 +81,7 @@ func (s *Server) handleComAtprotoModerationCreateReport(ctx context.Context, bod // we still want the report to go through! log.Error(ctx, "failed to make clip for report", "error", err) } else { - clipURL := fmt.Sprintf("https://%s/api/clip/%s/%s.mp4", s.cli.PublicHost, did, clipID) + clipURL := fmt.Sprintf("https://%s/api/clip/%s/%s.mp4", s.cli.BroadcasterHost, did, clipID) newReason := fmt.Sprintf("%s\n\nClip: %s", *body.Reason, clipURL) body.Reason = &newReason } diff --git a/pkg/spxrpc/com_atproto_repo.go b/pkg/spxrpc/com_atproto_repo.go index 2db8906a..0d68905b 100644 --- a/pkg/spxrpc/com_atproto_repo.go +++ b/pkg/spxrpc/com_atproto_repo.go @@ -95,7 +95,7 @@ func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo stri return &comatprototypes.RepoDescribeRepo_Output{ Handle: s.cli.MyDID(), Did: s.cli.MyDID(), - DidDoc: atproto.DIDDoc(s.cli.PublicHost), + DidDoc: atproto.DIDDoc(s.cli.BroadcasterHost), Collections: []string{ "com.atproto.lexicon.schema", }, diff --git a/pkg/spxrpc/com_atproto_server.go b/pkg/spxrpc/com_atproto_server.go index 8da207dd..0254a429 100644 --- a/pkg/spxrpc/com_atproto_server.go +++ b/pkg/spxrpc/com_atproto_server.go @@ -8,13 +8,13 @@ import ( ) func (s *Server) handleComAtprotoServerDescribeServer(ctx context.Context) (*comatprototypes.ServerDescribeServer_Output, error) { - did := fmt.Sprintf("did:web:%s", s.cli.PublicHost) + did := fmt.Sprintf("did:web:%s", s.cli.BroadcasterHost) trueVar := true return &comatprototypes.ServerDescribeServer_Output{ Did: did, InviteCodeRequired: &trueVar, AvailableUserDomains: []string{ - fmt.Sprintf(".%s", s.cli.PublicHost), + fmt.Sprintf(".%s", s.cli.BroadcasterHost), }, }, nil } diff --git a/pkg/streamplace/broadcastorigin.go b/pkg/streamplace/broadcastorigin.go new file mode 100644 index 00000000..778c6ae1 --- /dev/null +++ b/pkg/streamplace/broadcastorigin.go @@ -0,0 +1,25 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package streamplace + +// schema: place.stream.broadcast.origin + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + util.RegisterType("place.stream.broadcast.origin", &BroadcastOrigin{}) +} // +// RECORDTYPE: BroadcastOrigin +type BroadcastOrigin struct { + LexiconTypeID string `json:"$type,const=place.stream.broadcast.origin" cborgen:"$type,const=place.stream.broadcast.origin"` + // irohTicket: Iroh ticket that can be used to access the livestream from the server + IrohTicket *string `json:"irohTicket,omitempty" cborgen:"irohTicket,omitempty"` + // server: did:web of the server that's currently rebroadcasting the livestream + Server string `json:"server" cborgen:"server"` + // streamer: DID of the streamer whose livestream is being published + Streamer string `json:"streamer" cborgen:"streamer"` + // updatedAt: Periodically updated timestamp when this origin last saw a livestream + UpdatedAt string `json:"updatedAt" cborgen:"updatedAt"` +} diff --git a/pkg/streamplace/broadcastsyndication.go b/pkg/streamplace/broadcastsyndication.go new file mode 100644 index 00000000..8b839ddd --- /dev/null +++ b/pkg/streamplace/broadcastsyndication.go @@ -0,0 +1,23 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package streamplace + +// schema: place.stream.broadcast.syndication + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + util.RegisterType("place.stream.broadcast.syndication", &BroadcastSyndication{}) +} // +// RECORDTYPE: BroadcastSyndication +type BroadcastSyndication struct { + LexiconTypeID string `json:"$type,const=place.stream.broadcast.syndication" cborgen:"$type,const=place.stream.broadcast.syndication"` + // broadcaster: DID of the Streamplace broadcaster that will be replicating the livestream + Broadcaster string `json:"broadcaster" cborgen:"broadcaster"` + // createdAt: Client-declared timestamp when this syndication was created. + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + // streamer: DID of the streamer whose livestream is being replicated + Streamer string `json:"streamer" cborgen:"streamer"` +} diff --git a/pkg/streamplace/cbor_gen.go b/pkg/streamplace/cbor_gen.go index df32ed41..a99ce865 100644 --- a/pkg/streamplace/cbor_gen.go +++ b/pkg/streamplace/cbor_gen.go @@ -3147,3 +3147,457 @@ func (t *ChatGate) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *BroadcastOrigin) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 5 + + if t.IrohTicket == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.broadcast.origin"))); err != nil { + return err + } + if _, err := cw.WriteString(string("place.stream.broadcast.origin")); err != nil { + return err + } + + // t.Server (string) (string) + if len("server") > 1000000 { + return xerrors.Errorf("Value in field \"server\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("server"))); err != nil { + return err + } + if _, err := cw.WriteString(string("server")); err != nil { + return err + } + + if len(t.Server) > 1000000 { + return xerrors.Errorf("Value in field t.Server was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Server))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Server)); err != nil { + return err + } + + // t.Streamer (string) (string) + if len("streamer") > 1000000 { + return xerrors.Errorf("Value in field \"streamer\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamer"))); err != nil { + return err + } + if _, err := cw.WriteString(string("streamer")); err != nil { + return err + } + + if len(t.Streamer) > 1000000 { + return xerrors.Errorf("Value in field t.Streamer was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Streamer))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Streamer)); err != nil { + return err + } + + // t.UpdatedAt (string) (string) + if len("updatedAt") > 1000000 { + return xerrors.Errorf("Value in field \"updatedAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("updatedAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("updatedAt")); err != nil { + return err + } + + if len(t.UpdatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.UpdatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.UpdatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.UpdatedAt)); err != nil { + return err + } + + // t.IrohTicket (string) (string) + if t.IrohTicket != nil { + + if len("irohTicket") > 1000000 { + return xerrors.Errorf("Value in field \"irohTicket\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("irohTicket"))); err != nil { + return err + } + if _, err := cw.WriteString(string("irohTicket")); err != nil { + return err + } + + if t.IrohTicket == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.IrohTicket) > 1000000 { + return xerrors.Errorf("Value in field t.IrohTicket was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.IrohTicket))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.IrohTicket)); err != nil { + return err + } + } + } + return nil +} + +func (t *BroadcastOrigin) UnmarshalCBOR(r io.Reader) (err error) { + *t = BroadcastOrigin{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("BroadcastOrigin: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Server (string) (string) + case "server": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Server = string(sval) + } + // t.Streamer (string) (string) + case "streamer": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Streamer = string(sval) + } + // t.UpdatedAt (string) (string) + case "updatedAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.UpdatedAt = string(sval) + } + // t.IrohTicket (string) (string) + case "irohTicket": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.IrohTicket = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *BroadcastSyndication) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.broadcast.syndication"))); err != nil { + return err + } + if _, err := cw.WriteString(string("place.stream.broadcast.syndication")); err != nil { + return err + } + + // t.Streamer (string) (string) + if len("streamer") > 1000000 { + return xerrors.Errorf("Value in field \"streamer\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamer"))); err != nil { + return err + } + if _, err := cw.WriteString(string("streamer")); err != nil { + return err + } + + if len(t.Streamer) > 1000000 { + return xerrors.Errorf("Value in field t.Streamer was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Streamer))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Streamer)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.Broadcaster (string) (string) + if len("broadcaster") > 1000000 { + return xerrors.Errorf("Value in field \"broadcaster\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("broadcaster"))); err != nil { + return err + } + if _, err := cw.WriteString(string("broadcaster")); err != nil { + return err + } + + if len(t.Broadcaster) > 1000000 { + return xerrors.Errorf("Value in field t.Broadcaster was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Broadcaster))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Broadcaster)); err != nil { + return err + } + return nil +} + +func (t *BroadcastSyndication) UnmarshalCBOR(r io.Reader) (err error) { + *t = BroadcastSyndication{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("BroadcastSyndication: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Streamer (string) (string) + case "streamer": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Streamer = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.Broadcaster (string) (string) + case "broadcaster": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Broadcaster = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} -- 2.51.2 From 28702cadf4e27fc64267b5596a1c877dbfc3d764 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 4 Oct 2025 14:27:43 -0700 Subject: [PATCH 114/525] broadcast: index place.stream.broadcast.origin --- pkg/atproto/firehose.go | 10 +-- pkg/atproto/lexicon_repo.go | 3 +- pkg/atproto/sync.go | 15 ++++ pkg/director/stream_session.go | 110 +++++++++++++++++++++--- pkg/model/broadcast_origin.go | 54 ++++++++++++ pkg/model/model.go | 4 + pkg/{atproto/get_cid.go => spid/cid.go} | 2 +- pkg/spxrpc/place_stream_live.go | 4 +- 8 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 pkg/model/broadcast_origin.go rename pkg/{atproto/get_cid.go => spid/cid.go} (96%) diff --git a/pkg/atproto/firehose.go b/pkg/atproto/firehose.go index e3459be2..16c283c9 100644 --- a/pkg/atproto/firehose.go +++ b/pkg/atproto/firehose.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "runtime" + "strings" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -157,15 +158,9 @@ func (atsync *ATProtoSynchronizer) StartFirehoseRetry(ctx context.Context) error } var CollectionFilter = []string{ - constants.PLACE_STREAM_KEY, - constants.PLACE_STREAM_LIVESTREAM, - constants.PLACE_STREAM_CHAT_MESSAGE, - constants.PLACE_STREAM_CHAT_PROFILE, constants.APP_BSKY_GRAPH_FOLLOW, constants.APP_BSKY_FEED_POST, constants.APP_BSKY_GRAPH_BLOCK, - constants.PLACE_STREAM_SERVER_SETTINGS, - constants.PLACE_STREAM_CHAT_GATE, } func (atsync *ATProtoSynchronizer) handleCommitEventOps(ctx context.Context, evt *comatproto.SyncSubscribeRepos_Commit) { @@ -195,6 +190,9 @@ func (atsync *ATProtoSynchronizer) handleCommitEventOps(ctx context.Context, evt if len(CollectionFilter) > 0 { keep := slices.Contains(CollectionFilter, collection.String()) + if strings.HasPrefix(collection.String(), "place.stream.") { + keep = true + } if !keep { continue } diff --git a/pkg/atproto/lexicon_repo.go b/pkg/atproto/lexicon_repo.go index b587005d..679a1a7a 100644 --- a/pkg/atproto/lexicon_repo.go +++ b/pkg/atproto/lexicon_repo.go @@ -29,6 +29,7 @@ import ( "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/model" + "stream.place/streamplace/pkg/spid" "stream.place/streamplace/pkg/statedb" ) @@ -247,7 +248,7 @@ func MakeLexiconRepo(ctx context.Context, cli *config.CLI, mod model.Model, stat } sfw := &SchemaFileWrapper{SchemaFile: lexFile} rpath := fmt.Sprintf("com.atproto.lexicon.schema/%s", lexFile.ID) - newCid, err := GetCID(sfw) + newCid, err := spid.GetCID(sfw) if err != nil { return nil, err } diff --git a/pkg/atproto/sync.go b/pkg/atproto/sync.go index db4b21a1..a6927a4f 100644 --- a/pkg/atproto/sync.go +++ b/pkg/atproto/sync.go @@ -389,6 +389,21 @@ func (atsync *ATProtoSynchronizer) handleCreateUpdate(ctx context.Context, userD log.Error(ctx, "failed to create signing key", "err", err) } + case *streamplace.BroadcastOrigin: + log.Warn(ctx, "creating broadcast origin", "origin", rec) + _, err := atsync.SyncBlueskyRepoCached(ctx, userDID, atsync.Model) + if err != nil { + return fmt.Errorf("failed to sync broadcast origin creator bluesky repo: %w", err) + } + _, err = atsync.SyncBlueskyRepoCached(ctx, rec.Streamer, atsync.Model) + if err != nil { + return fmt.Errorf("failed to sync broadcast origin streamer bluesky repo: %w", err) + } + err = atsync.Model.UpdateBroadcastOrigin(ctx, rec, aturi) + if err != nil { + log.Error(ctx, "failed to update broadcast origin", "err", err) + } + default: log.Debug(ctx, "unhandled record type", "type", reflect.TypeOf(rec)) } diff --git a/pkg/director/stream_session.go b/pkg/director/stream_session.go index b4baa49a..c0980dce 100644 --- a/pkg/director/stream_session.go +++ b/pkg/director/stream_session.go @@ -40,6 +40,8 @@ type StreamSession struct { segmentChan chan struct{} lastStatus time.Time lastStatusLock sync.Mutex + lastOriginTime time.Time + lastOriginLock sync.Mutex g *errgroup.Group started chan struct{} ctx context.Context @@ -48,7 +50,7 @@ type StreamSession struct { swarm *iroh_replicator.IrohSwarm } -func (ss *StreamSession) Start(ctx context.Context, not *media.NewSegmentNotification) error { +func (ss *StreamSession) Start(ctx context.Context, notif *media.NewSegmentNotification) error { ctx, cancel := context.WithCancel(ctx) ss.g, ctx = errgroup.WithContext(ctx) sid := livepeer.RandomTrailer(8) @@ -56,7 +58,7 @@ func (ss *StreamSession) Start(ctx context.Context, not *media.NewSegmentNotific ss.ctx = ctx log.Log(ctx, "starting stream session") defer cancel() - spseg, err := not.Segment.ToStreamplaceSegment() + spseg, err := notif.Segment.ToStreamplaceSegment() if err != nil { return fmt.Errorf("could not convert segment to streamplace segment: %w", err) } @@ -74,7 +76,7 @@ func (ss *StreamSession) Start(ctx context.Context, not *media.NewSegmentNotific return fmt.Errorf("segment duration is required to calculate bitrate") } dur := time.Duration(*spseg.Duration) - byteLen := len(not.Data) + byteLen := len(notif.Data) bitrate := int(float64(byteLen) / dur.Seconds() * 8) sourceRendition := renditions.Rendition{ Name: "source", @@ -134,7 +136,7 @@ func (ss *StreamSession) Go(ctx context.Context, f func() error) { }) } -func (ss *StreamSession) NewSegment(ctx context.Context, not *media.NewSegmentNotification) error { +func (ss *StreamSession) NewSegment(ctx context.Context, notif *media.NewSegmentNotification) error { <-ss.started go func() { select { @@ -143,14 +145,14 @@ func (ss *StreamSession) NewSegment(ctx context.Context, not *media.NewSegmentNo case ss.segmentChan <- struct{}{}: } }() - aqt := aqtime.FromTime(not.Segment.StartTime) - ctx = log.WithLogValues(ctx, "segID", not.Segment.ID, "repoDID", not.Segment.RepoDID, "timestamp", aqt.FileSafeString()) - not.Segment.MediaData.Size = len(not.Data) - err := ss.mod.CreateSegment(not.Segment) + aqt := aqtime.FromTime(notif.Segment.StartTime) + ctx = log.WithLogValues(ctx, "segID", notif.Segment.ID, "repoDID", notif.Segment.RepoDID, "timestamp", aqt.FileSafeString()) + notif.Segment.MediaData.Size = len(notif.Data) + err := ss.mod.CreateSegment(notif.Segment) if err != nil { return fmt.Errorf("could not add segment to database: %w", err) } - spseg, err := not.Segment.ToStreamplaceSegment() + spseg, err := notif.Segment.ToStreamplaceSegment() if err != nil { return fmt.Errorf("could not convert segment to streamplace segment: %w", err) } @@ -158,14 +160,14 @@ func (ss *StreamSession) NewSegment(ctx context.Context, not *media.NewSegmentNo ss.bus.Publish(spseg.Creator, spseg) ss.Go(ctx, func() error { return ss.AddPlaybackSegment(ctx, spseg, "source", &bus.Seg{ - Filepath: not.Segment.ID, - Data: not.Data, + Filepath: notif.Segment.ID, + Data: notif.Data, }) }) if ss.cli.Thumbnail { ss.Go(ctx, func() error { - return ss.Thumbnail(ctx, spseg.Creator, not) + return ss.Thumbnail(ctx, spseg.Creator, notif) }) } @@ -173,10 +175,16 @@ func (ss *StreamSession) NewSegment(ctx context.Context, not *media.NewSegmentNo return ss.UpdateStatus(ctx, spseg.Creator) }) + if notif.Local { + ss.Go(ctx, func() error { + return ss.UpdateBroadcastOrigin(ctx) + }) + } + if ss.cli.LivepeerGatewayURL != "" { ss.Go(ctx, func() error { start := time.Now() - err := ss.Transcode(ctx, spseg, not.Data) + err := ss.Transcode(ctx, spseg, notif.Data) took := time.Since(start) spmetrics.QueuedTranscodeDuration.WithLabelValues(spseg.Creator).Set(float64(took.Milliseconds())) return err @@ -375,6 +383,82 @@ func (ss *StreamSession) UpdateStatus(ctx context.Context, repoDID string) error return nil } +var originUpdateInterval = time.Second * 30 + +func (ss *StreamSession) UpdateBroadcastOrigin(ctx context.Context) error { + ctx = log.WithLogValues(ctx, "func", "UpdateStatus") + ss.lastOriginLock.Lock() + defer ss.lastOriginLock.Unlock() + if time.Since(ss.lastOriginTime) < originUpdateInterval { + log.Debug(ctx, "not updating origin, last origin was less than 30 seconds ago") + return nil + } + origin := streamplace.BroadcastOrigin{ + Streamer: ss.repoDID, + Server: fmt.Sprintf("did:web:%s", ss.cli.BroadcasterHost), + UpdatedAt: time.Now().Format(time.RFC3339), + IrohTicket: &ss.swarm.NodeTicket, + } + + session, err := ss.statefulDB.GetSessionByDID(ss.repoDID) + if err != nil { + return fmt.Errorf("could not get OAuth session for repoDID: %w", err) + } + if session == nil { + return fmt.Errorf("no session found for repoDID: %s", ss.repoDID) + } + + session, err = ss.op.RefreshIfNeeded(session) + if err != nil { + return fmt.Errorf("could not refresh session for repoDID: %w", err) + } + + client, err := ss.op.GetXrpcClient(session) + if err != nil { + return fmt.Errorf("could not get xrpc client: %w", err) + } + + rkey := fmt.Sprintf("%s::did:web:%s", ss.repoDID, ss.cli.BroadcasterHost) + + var swapRecord *string + getOutput := atproto.RepoGetRecord_Output{} + err = client.Do(ctx, xrpc.Query, "application/json", "com.atproto.repo.getRecord", map[string]any{ + "repo": ss.repoDID, + "collection": "place.stream.broadcast.origin", + "rkey": rkey, + }, nil, &getOutput) + if err != nil { + xErr, ok := err.(*xrpc.Error) + if !ok { + return fmt.Errorf("could not get record: %w", err) + } + if xErr.StatusCode != 400 { // yes, they return "400" for "not found" + return fmt.Errorf("could not get record: %w", err) + } + log.Debug(ctx, "record not found, creating", "repoDID", ss.repoDID) + } else { + log.Debug(ctx, "got record", "record", getOutput) + swapRecord = getOutput.Cid + } + + inp := atproto.RepoPutRecord_Input{ + Collection: "place.stream.broadcast.origin", + Record: &util.LexiconTypeDecoder{Val: &origin}, + Rkey: fmt.Sprintf("%s::did:web:%s", ss.repoDID, ss.cli.BroadcasterHost), + Repo: ss.repoDID, + SwapRecord: swapRecord, + } + out := atproto.RepoPutRecord_Output{} + + err = client.Do(ctx, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", map[string]any{}, inp, &out) + if err != nil { + return fmt.Errorf("could not create record: %w", err) + } + + ss.lastOriginTime = time.Now() + return nil +} + func (ss *StreamSession) Transcode(ctx context.Context, spseg *streamplace.Segment, data []byte) error { rs, err := renditions.GenerateRenditions(spseg) if err != nil { diff --git a/pkg/model/broadcast_origin.go b/pkg/model/broadcast_origin.go new file mode 100644 index 00000000..66f5e5b1 --- /dev/null +++ b/pkg/model/broadcast_origin.go @@ -0,0 +1,54 @@ +package model + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/bluesky-social/indigo/atproto/syntax" + "stream.place/streamplace/pkg/spid" + "stream.place/streamplace/pkg/streamplace" +) + +type BroadcastOrigin struct { + URI string `gorm:"primaryKey;column:uri"` + CID string `gorm:"column:cid"` + RepoDID string `gorm:"column:repo_did"` + Repo *Repo `gorm:"foreignKey:DID;references:RepoDID"` + StreamerRepoDID string `gorm:"column:streamer_repo_did;index:idx_streamer_repo_did_updated_at,priority:1"` + StreamerRepo *Repo `gorm:"foreignKey:DID;references:StreamerRepoDID"` + ServerRepoDID string `gorm:"column:server_repo_did;index:idx_server_repo_did_updated_at,priority:1"` + UpdatedAt time.Time `gorm:"column:updated_at;index:idx_streamer_repo_did_updated_at,priority:2;index:idx_server_repo_did_updated_at,priority:2"` + Record []byte `gorm:"column:record"` +} + +func (bo *BroadcastOrigin) TableName() string { + return "broadcast_origins" +} + +func (m *DBModel) UpdateBroadcastOrigin(ctx context.Context, origin *streamplace.BroadcastOrigin, aturi syntax.ATURI) error { + repoDID := aturi.Authority().String() + cid, err := spid.GetCID(origin) + if err != nil { + return fmt.Errorf("failed to get CID: %w", err) + } + validATURI := fmt.Sprintf("at://%s/place.stream.broadcast.origin/%s::%s", repoDID, origin.Streamer, origin.Server) + if validATURI != aturi.String() { + return fmt.Errorf("invalid ATURI: %s != %s", validATURI, aturi.String()) + } + buf := bytes.Buffer{} + err = origin.MarshalCBOR(&buf) + if err != nil { + return fmt.Errorf("failed to marshal origin: %w", err) + } + bo := &BroadcastOrigin{ + URI: validATURI, + CID: cid.String(), + StreamerRepoDID: origin.Streamer, + ServerRepoDID: origin.Server, + UpdatedAt: time.Now(), + Record: buf.Bytes(), + } + return m.DB.Save(bo).Error +} diff --git a/pkg/model/model.go b/pkg/model/model.go index f5251953..f18b94af 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -9,6 +9,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" "gorm.io/driver/sqlite" "gorm.io/gorm" "stream.place/streamplace/pkg/config" @@ -96,6 +97,8 @@ type Model interface { CreateLabel(label *Label) error GetActiveLabels(uri string) ([]*comatproto.LabelDefs_Label, error) + + UpdateBroadcastOrigin(ctx context.Context, origin *streamplace.BroadcastOrigin, aturi syntax.ATURI) error } var DBRevision = 2 @@ -152,6 +155,7 @@ func MakeDB(dbURL string) (Model, error) { ServerSettings{}, Labeler{}, Label{}, + BroadcastOrigin{}, } { err = db.AutoMigrate(model) if err != nil { diff --git a/pkg/atproto/get_cid.go b/pkg/spid/cid.go similarity index 96% rename from pkg/atproto/get_cid.go rename to pkg/spid/cid.go index 7b08e555..22070dbb 100644 --- a/pkg/atproto/get_cid.go +++ b/pkg/spid/cid.go @@ -1,4 +1,4 @@ -package atproto +package spid import ( "bytes" diff --git a/pkg/spxrpc/place_stream_live.go b/pkg/spxrpc/place_stream_live.go index b2721840..36705a98 100644 --- a/pkg/spxrpc/place_stream_live.go +++ b/pkg/spxrpc/place_stream_live.go @@ -8,7 +8,7 @@ import ( "github.com/bluesky-social/indigo/lex/util" "github.com/labstack/echo/v4" - "stream.place/streamplace/pkg/atproto" + "stream.place/streamplace/pkg/spid" "stream.place/streamplace/pkg/spmetrics" placestreamtypes "stream.place/streamplace/pkg/streamplace" @@ -42,7 +42,7 @@ func (s *Server) handlePlaceStreamLiveGetSegments(ctx context.Context, before st if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to convert segment to streamplace segment: %s", err)) } - c, err := atproto.GetCID(record) + c, err := spid.GetCID(record) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get CID: %s", err)) } -- 2.51.2 From f7aa2148b2680ecb1004549ea75e5fcc16c6ae16 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 5 Oct 2025 15:14:00 -0700 Subject: [PATCH 115/525] log: fix truncated --help from livepeer logging --- pkg/config/config.go | 2 ++ pkg/log/log.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index f692e80d..b4264829 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,6 +28,7 @@ import ( "stream.place/streamplace/pkg/constants" "stream.place/streamplace/pkg/crypto/aqpub" "stream.place/streamplace/pkg/integrations/discord/discordtypes" + "stream.place/streamplace/pkg/log" ) const SPDataDir = "$SP_DATA_DIR" @@ -272,6 +273,7 @@ func (cli *CLI) Parse(fs *flag.FlagSet, args []string) error { return fmt.Errorf("defining both livepeer-gateway and livepeer-gateway-url doesn't make sense. do you want an embedded gateway or an external one?") } if cli.LivepeerGateway { + log.MonkeypatchStderr() gatewayPath := cli.DataFilePath([]string{"livepeer", "gateway"}) err = fs.Set("livepeer.rtmp-addr", "127.0.0.1:0") if err != nil { diff --git a/pkg/log/log.go b/pkg/log/log.go index 1b2a4367..23256b2e 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -72,7 +72,7 @@ func SetColorLogger(color string) { )) } -func init() { +func MonkeypatchStderr() { r, w, err := os.Pipe() if err != nil { panic(err) -- 2.51.2 From b189242d83e06b1ae0536f814a3bc2e712b6a915 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 5 Oct 2025 21:47:56 -0700 Subject: [PATCH 116/525] metrics: add protocol to viewers --- pkg/media/webrtc_playback.go | 4 ++-- pkg/media/webrtc_playback2.go | 4 ++-- pkg/spmetrics/spmetrics.go | 37 +++++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/pkg/media/webrtc_playback.go b/pkg/media/webrtc_playback.go index d25d998d..b5538c8f 100644 --- a/pkg/media/webrtc_playback.go +++ b/pkg/media/webrtc_playback.go @@ -303,8 +303,8 @@ func (mm *MediaManager) WebRTCPlayback(ctx context.Context, user string, renditi if err != nil { log.Log(ctx, "failed to set pipeline state to null", "error", err) } - spmetrics.ViewerInc(user) - defer spmetrics.ViewerDec(user) + spmetrics.ViewerInc(user, "webrtc") + defer spmetrics.ViewerDec(user, "webrtc") go func() { rtcpBuf := make([]byte, 1500) diff --git a/pkg/media/webrtc_playback2.go b/pkg/media/webrtc_playback2.go index c53eff72..e567f6fb 100644 --- a/pkg/media/webrtc_playback2.go +++ b/pkg/media/webrtc_playback2.go @@ -181,8 +181,8 @@ func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendit } }() - spmetrics.ViewerInc(user) - defer spmetrics.ViewerDec(user) + spmetrics.ViewerInc(user, "webrtc") + defer spmetrics.ViewerDec(user, "webrtc") go func() { rtcpBuf := make([]byte, 1500) diff --git a/pkg/spmetrics/spmetrics.go b/pkg/spmetrics/spmetrics.go index 3523997c..a7603a9e 100644 --- a/pkg/spmetrics/spmetrics.go +++ b/pkg/spmetrics/spmetrics.go @@ -12,7 +12,8 @@ import ( const SessionExpireTime = 30 * time.Second //nolint:all -var viewers = map[string]int{} +var viewersByStreamer = map[string]int{} +var viewersByProtocol = map[string]int{} var viewersLock sync.RWMutex var sessions = map[string]map[string]time.Time{} @@ -22,10 +23,10 @@ var Viewers = promauto.NewGaugeVec(prometheus.GaugeOpts{ Help: "number of current viewers per user", }, []string{"streamer"}) -var ViewersTotal = promauto.NewGauge(prometheus.GaugeOpts{ +var ViewersTotal = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "streamplace_viewers_total", Help: "total number of viewers", -}) +}, []string{"protocol"}) var TranscodeAttemptsTotal = promauto.NewCounter(prometheus.CounterOpts{ Name: "streamplace_transcode_attempts_total", @@ -74,34 +75,40 @@ var SegmentSubscriptionsOpen = promauto.NewGaugeVec(prometheus.GaugeOpts{ Help: "number of open new segment subscriptions", }, []string{"streamer", "rendition"}) -func ViewerInc(user string) { +func ViewerInc(user string, protocol string) { go func() { viewersLock.Lock() defer viewersLock.Unlock() - viewers[user]++ - Viewers.WithLabelValues(user).Set(float64(viewers[user])) - ViewersTotal.Inc() + viewersByStreamer[user]++ + viewersByProtocol[protocol]++ + Viewers.WithLabelValues(user).Set(float64(viewersByStreamer[user])) + ViewersTotal.WithLabelValues(protocol).Set(float64(viewersByProtocol[protocol])) }() } -func ViewerDec(user string) { +func ViewerDec(user string, protocol string) { go func() { viewersLock.Lock() defer viewersLock.Unlock() - viewers[user]-- - if viewers[user] == 0 { + viewersByStreamer[user]-- + if viewersByStreamer[user] == 0 { Viewers.DeleteLabelValues(user) } else { - Viewers.WithLabelValues(user).Set(float64(viewers[user])) + Viewers.WithLabelValues(user).Set(float64(viewersByStreamer[user])) + } + viewersByProtocol[protocol]-- + if viewersByProtocol[protocol] == 0 { + Viewers.DeleteLabelValues(protocol) + } else { + Viewers.WithLabelValues(protocol).Set(float64(viewersByProtocol[protocol])) } - ViewersTotal.Dec() }() } func GetViewCount(user string) int { viewersLock.RLock() defer viewersLock.RUnlock() - return viewers[user] + return viewersByStreamer[user] } func SessionSeen(user string, session string) { @@ -114,7 +121,7 @@ func SessionSeen(user string, session string) { } if _, ok := sessions[user][session]; !ok { log.Warn(context.TODO(), "ViewerInc", "user", user, "session", session) - ViewerInc(user) + ViewerInc(user, "hls") } sessions[user][session] = now }() @@ -131,7 +138,7 @@ func ExpireSessions(ctx context.Context) error { for session, seen := range sessions { if time.Since(seen) > SessionExpireTime { delete(sessions, session) - ViewerDec(user) + ViewerDec(user, "hls") } } } -- 2.51.2 From 39a263c4489dde5abe6eaf63199be3e062d8dff5 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Thu, 11 Sep 2025 14:35:21 -0700 Subject: [PATCH 117/525] oauth: hacks for localhost dev --- go.mod | 4 ++ go.sum | 4 -- js/app/features/bluesky/blueskySlice.tsx | 5 ++ js/app/features/bluesky/oauthClient.tsx | 77 +++++++++++++++++------- pkg/cmd/streamplace.go | 16 ++++- pkg/config/config.go | 12 ++++ 6 files changed, 90 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 16bbd390..96eeb1a1 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,10 @@ replace github.com/AxisCommunications/go-dpop => github.com/streamplace/go-dpop replace github.com/bluesky-social/indigo => github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b +replace github.com/streamplace/oatproxy => ../oatproxy + +replace github.com/streamplace/atproto-oauth-golang => ../atproto-oauth-golang + require ( firebase.google.com/go/v4 v4.14.1 github.com/99designs/gqlgen v0.17.64 diff --git a/go.sum b/go.sum index 0a1ba897..adf679b5 100644 --- a/go.sum +++ b/go.sum @@ -1271,14 +1271,10 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= -github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac h1:heVM4CGox3kfJclSmagsI3hvKk7W52EegnFqqs9CSYk= -github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac/go.mod h1:9LlKkqciiO5lRfbX0n4Wn5KNY9nvFb4R3by8FdW2TWc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b h1:vh0Z1bDgcVXQjgmcXOBkC3B4uWi+HaxeyTvETzcA/Vg= github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= -github.com/streamplace/oatproxy v0.0.0-20250903220936-3131b72481a6 h1:s83q+spLKCE/UXZEDnU7FEt6PJxyH+utQCyvQILATZk= -github.com/streamplace/oatproxy v0.0.0-20250903220936-3131b72481a6/go.mod h1:pXi24hA7xBHj8eEywX6wGqJOR9FaEYlGwQ/72rN6okw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= diff --git a/js/app/features/bluesky/blueskySlice.tsx b/js/app/features/bluesky/blueskySlice.tsx index becb8904..94de6b98 100644 --- a/js/app/features/bluesky/blueskySlice.tsx +++ b/js/app/features/bluesky/blueskySlice.tsx @@ -253,6 +253,11 @@ export const blueskySlice = createAppSlice({ throw new Error("No client"); } const u = await bluesky.client.authorize(handle, {}); + if (document.location.href.startsWith("http://127.0.0.1")) { + const hostUrl = new URL(document.location.href); + u.host = hostUrl.host; + u.protocol = hostUrl.protocol; + } thunkAPI.dispatch(openLoginLink(u.toString())); // cheeky 500ms delay so you don't see the text flash back await new Promise((resolve) => setTimeout(resolve, 5000)); diff --git a/js/app/features/bluesky/oauthClient.tsx b/js/app/features/bluesky/oauthClient.tsx index 7c49f262..0f72a588 100644 --- a/js/app/features/bluesky/oauthClient.tsx +++ b/js/app/features/bluesky/oauthClient.tsx @@ -1,3 +1,4 @@ +import { BrowserOAuthClient } from "@atproto/oauth-client-browser"; import { ClientMetadata, clientMetadataSchema, @@ -70,8 +71,9 @@ export default async function createOAuthClient( meta = await res.json(); } clientMetadataSchema.parse(meta); - return new ReactNativeOAuthClient({ + return new BrowserOAuthClient({ fetch: async (input, init) => { + console.log("fetch", input, init); // Normalize input to a Request object let request: Request; if (typeof input === "string" || input instanceof URL) { @@ -79,29 +81,62 @@ export default async function createOAuthClient( } else { request = input; } - - // Lie to the oauth client and use our upstream server instead - if ( - request.url.includes("plc.directory") || - request.url.endsWith("did.json") - ) { - const res = await fetch(request, init); - if (!res.ok) { - return res; + if (streamplaceUrl.startsWith("http://127.0.0.1")) { + // everything other than PDS resolution gets rewritten to the host + if ( + request.url.includes("plc.directory") || + request.url.endsWith("did.json") || + request.url.endsWith("/.well-known/oauth-protected-resource") || + request.url.endsWith("/.well-known/oauth-authorization-server") + ) { + return fetch(request, init) as any; + } + const newUrl = new URL(request.url.toString()); + newUrl.protocol = "http:"; + newUrl.host = "127.0.0.1:38080"; + let newRequest: Request; + if (request.method === "POST") { + const data = await request.blob(); + newRequest = new Request(newUrl.toString(), { + body: data, + method: "POST", + headers: request.headers, + }); + } else if (request.method === "GET") { + newRequest = new Request(newUrl.toString(), { + method: "GET", + headers: request.headers, + }); + } else { + throw new Error("Unsupported method: " + request.method); } - const data = await res.json(); - const service = data.service.find((s: any) => s.id === "#atproto_pds"); - if (!service) { - return res; + return fetch(newRequest) as any; + } else { + // Lie to the oauth client and use our upstream server instead + if ( + request.url.includes("plc.directory") || + request.url.endsWith("did.json") + ) { + const res = await fetch(request, init); + if (!res.ok) { + return res; + } + const data = await res.json(); + const service = data.service.find( + (s: any) => s.id === "#atproto_pds", + ); + if (!service) { + return res; + } + service.serviceEndpoint = "https://example.com"; + console.log("returning", data); + return new Response(JSON.stringify(data), { + status: res.status, + headers: res.headers, + }); + return fetch(request, init); } - service.serviceEndpoint = streamplaceUrl; - return new Response(JSON.stringify(data), { - status: res.status, - headers: res.headers, - }); } - - return fetch(request, init); }, handleResolver: streamplaceUrl, responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only) diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index d3ad3589..6aa6fc97 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -386,17 +386,26 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { return err } + // clientMetadata := &oatproxy.OAuthClientMetadata{ + // Scope: "atproto transition:generic", + // ClientName: "Streamplace", + // RedirectURIs: []string{ + // fmt.Sprintf("https://%s/login", cli.PublicHost), + // fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), + // }, + // } + clientMetadata := &oatproxy.OAuthClientMetadata{ Scope: "atproto transition:generic", ClientName: "Streamplace", RedirectURIs: []string{ - fmt.Sprintf("https://%s/login", cli.PublicHost), - fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), + fmt.Sprintf("%s/login", cli.OwnPublicURL()), + fmt.Sprintf("%s/api/app-return", cli.OwnPublicURL()), }, } op := oatproxy.New(&oatproxy.Config{ - Host: cli.PublicHost, + Host: "127.0.0.1:38080", CreateOAuthSession: state.CreateOAuthSession, UpdateOAuthSession: state.UpdateOAuthSession, GetOAuthSession: state.LoadOAuthSession, @@ -405,6 +414,7 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { UpstreamJWK: cli.JWK, DownstreamJWK: cli.AccessJWK, ClientMetadata: clientMetadata, + Public: true, }) d := director.NewDirector(mm, mod, &cli, b, op, state) a, err := api.MakeStreamplaceAPI(&cli, mod, state, eip712signer, noter, mm, ms, b, atsync, d, op) diff --git a/pkg/config/config.go b/pkg/config/config.go index f692e80d..21c4745d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -203,6 +203,18 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { var StreamplaceSchemePrefix = "streamplace://" +func (cli *CLI) OwnPublicURL() string { + // No errors because we know it's valid from AddrFlag + host, port, _ := net.SplitHostPort(cli.HTTPAddr) + + ip := net.ParseIP(host) + if host == "" || ip.IsUnspecified() { + host = "127.0.0.1" + } + addr := net.JoinHostPort(host, port) + return fmt.Sprintf("http://%s", addr) +} + func (cli *CLI) OwnInternalURL() string { // No errors because we know it's valid from AddrFlag host, port, _ := net.SplitHostPort(cli.HTTPInternalAddr) -- 2.51.2 From 36cef7166be3d5c0c2247d4844e9fe836ed6428c Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 6 Oct 2025 00:12:56 -0700 Subject: [PATCH 118/525] config: implement --dev-public-oauth --- .vscode/settings.json | 6 +++--- go.mod | 6 +----- go.sum | 6 ++++++ pkg/cmd/streamplace.go | 35 +++++++++++++++++++---------------- pkg/config/config.go | 2 ++ util/streamplace-dev.sh | 1 + 6 files changed, 32 insertions(+), 24 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d459a8b..4b90826e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,11 +10,11 @@ }, "mesonbuild.configureOnOpen": false, "cSpell.words": ["Devplace", "streamplace", "webrtc"], - "go.lintTool": "golangci-lint", - "go.lintFlags": ["--path-mode=abs"], + "go.lintTool": "golangci-lint-v2", + "go.lintFlags": ["--path-mode=abs", "--fast-only"], "go.formatTool": "custom", "go.alternateTools": { - "customFormatter": "golangci-lint" + "customFormatter": "golangci-lint-v2" }, "go.formatFlags": ["fmt", "--stdin"], "editor.codeActionsOnSave": { diff --git a/go.mod b/go.mod index 96eeb1a1..a4d590bb 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,6 @@ replace github.com/AxisCommunications/go-dpop => github.com/streamplace/go-dpop replace github.com/bluesky-social/indigo => github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b -replace github.com/streamplace/oatproxy => ../oatproxy - -replace github.com/streamplace/atproto-oauth-golang => ../atproto-oauth-golang - require ( firebase.google.com/go/v4 v4.14.1 github.com/99designs/gqlgen v0.17.64 @@ -59,7 +55,7 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/slok/go-http-metrics v0.13.0 github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac - github.com/streamplace/oatproxy v0.0.0-20250903220936-3131b72481a6 + github.com/streamplace/oatproxy v0.0.0-20251006070721-fdc391588c37 github.com/stretchr/testify v1.10.0 github.com/tdewolff/canvas v0.0.0-20250728095813-50d4cb1eee71 github.com/whyrusleeping/cbor-gen v0.3.1 diff --git a/go.sum b/go.sum index adf679b5..cfa366d0 100644 --- a/go.sum +++ b/go.sum @@ -1271,10 +1271,16 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac h1:heVM4CGox3kfJclSmagsI3hvKk7W52EegnFqqs9CSYk= +github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac/go.mod h1:9LlKkqciiO5lRfbX0n4Wn5KNY9nvFb4R3by8FdW2TWc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b h1:vh0Z1bDgcVXQjgmcXOBkC3B4uWi+HaxeyTvETzcA/Vg= github.com/streamplace/indigo v0.0.0-20250813192504-b19ccd82854b/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= +github.com/streamplace/oatproxy v0.0.0-20250903220936-3131b72481a6 h1:s83q+spLKCE/UXZEDnU7FEt6PJxyH+utQCyvQILATZk= +github.com/streamplace/oatproxy v0.0.0-20250903220936-3131b72481a6/go.mod h1:pXi24hA7xBHj8eEywX6wGqJOR9FaEYlGwQ/72rN6okw= +github.com/streamplace/oatproxy v0.0.0-20251006070721-fdc391588c37 h1:mCAT+PWrV1E/rbmA73/xYLnFw2DmOd6/HRR1AxZ1jRY= +github.com/streamplace/oatproxy v0.0.0-20251006070721-fdc391588c37/go.mod h1:pXi24hA7xBHj8eEywX6wGqJOR9FaEYlGwQ/72rN6okw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index 6aa6fc97..3399f721 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -386,22 +386,25 @@ func start(build *config.BuildFlags, platformJobs []jobFunc) error { return err } - // clientMetadata := &oatproxy.OAuthClientMetadata{ - // Scope: "atproto transition:generic", - // ClientName: "Streamplace", - // RedirectURIs: []string{ - // fmt.Sprintf("https://%s/login", cli.PublicHost), - // fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), - // }, - // } - - clientMetadata := &oatproxy.OAuthClientMetadata{ - Scope: "atproto transition:generic", - ClientName: "Streamplace", - RedirectURIs: []string{ - fmt.Sprintf("%s/login", cli.OwnPublicURL()), - fmt.Sprintf("%s/api/app-return", cli.OwnPublicURL()), - }, + var clientMetadata *oatproxy.OAuthClientMetadata + if cli.PublicOAuth { + clientMetadata = &oatproxy.OAuthClientMetadata{ + Scope: "atproto transition:generic", + ClientName: "Streamplace", + RedirectURIs: []string{ + fmt.Sprintf("%s/login", cli.OwnPublicURL()), + fmt.Sprintf("%s/api/app-return", cli.OwnPublicURL()), + }, + } + } else { + clientMetadata = &oatproxy.OAuthClientMetadata{ + Scope: "atproto transition:generic", + ClientName: "Streamplace", + RedirectURIs: []string{ + fmt.Sprintf("https://%s/login", cli.PublicHost), + fmt.Sprintf("https://%s/api/app-return", cli.PublicHost), + }, + } } op := oatproxy.New(&oatproxy.Config{ diff --git a/pkg/config/config.go b/pkg/config/config.go index 21c4745d..242f09c0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -88,6 +88,7 @@ type CLI struct { Redirects []string TestStream bool FrontendProxy string + PublicOAuth bool AppBundleID string NoFirehose bool PrintChat bool @@ -149,6 +150,7 @@ func (cli *CLI) NewFlagSet(name string) *flag.FlagSet { fs.StringVar(&cli.AppBundleID, "app-bundle-id", "", "bundle id of an app that we facilitate oauth login for") fs.StringVar(&cli.StreamerName, "streamer-name", "", "name of the person streaming from this streamplace node") fs.StringVar(&cli.FrontendProxy, "dev-frontend-proxy", "", "(FOR DEVELOPMENT ONLY) proxy frontend requests to this address instead of using the bundled frontend") + fs.BoolVar(&cli.PublicOAuth, "dev-public-oauth", false, "(FOR DEVELOPMENT ONLY) enable public oauth login for http://127.0.0.1 development") fs.StringVar(&cli.LivepeerGatewayURL, "livepeer-gateway-url", "", "URL of the Livepeer Gateway to use for transcoding") fs.BoolVar(&cli.LivepeerGateway, "livepeer-gateway", false, "enable embedded Livepeer Gateway") fs.BoolVar(&cli.WideOpen, "wide-open", false, "allow ALL streams to be uploaded to this node (not recommended for production)") diff --git a/util/streamplace-dev.sh b/util/streamplace-dev.sh index 37718730..c283337d 100755 --- a/util/streamplace-dev.sh +++ b/util/streamplace-dev.sh @@ -9,4 +9,5 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) STREAMPLACE_DEV_VERSION=$(go run ./pkg/config/git/git.go -v) \ LD_LIBRARY_PATH="$SCRIPT_DIR/lib" \ SP_DEV_FRONTEND_PROXY="http://127.0.0.1:38081" \ +SP_DEV_PUBLIC_OAUTH=true \ exec "$SCRIPT_DIR/libstreamplace" "$@" -- 2.51.2 From e6741c437dbd7e921412f2cad50aab3b7f4efb13 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 18:26:49 -0500 Subject: [PATCH 119/525] tost --- js/app/components/settings/settings.tsx | 11 +- js/components/src/components/ui/toast.tsx | 335 ++++++++++++++++------ js/components/src/lib/theme/theme.tsx | 7 + 3 files changed, 257 insertions(+), 96 deletions(-) diff --git a/js/app/components/settings/settings.tsx b/js/app/components/settings/settings.tsx index 7cbafc55..c89bd50a 100644 --- a/js/app/components/settings/settings.tsx +++ b/js/app/components/settings/settings.tsx @@ -1,5 +1,12 @@ import { useNavigation } from "@react-navigation/native"; -import { Button, Input, Text, View, zero } from "@streamplace/components"; +import { + Button, + Input, + Text, + useToast, + View, + zero, +} from "@streamplace/components"; import AQLink from "components/aqlink"; import { createServerSettingsRecord, @@ -21,6 +28,7 @@ export function Settings() { const defaultUrl = DEFAULT_URL; const [newUrl, setNewUrl] = useState(""); const [overrideEnabled, setOverrideEnabled] = useState(false); + const t = useToast(); // are we logged in? const loggedIn = useAppSelector( @@ -164,6 +172,7 @@ export function Settings() { )} + ); diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index dc2af75d..bf99b42f 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -1,18 +1,23 @@ import { Portal } from "@rn-primitives/portal"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { - Animated, Platform, Pressable, StyleSheet, - Text, useWindowDimensions, View, ViewStyle, } from "react-native"; +import Animated, { + Easing, + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; - import { useTheme } from "../../ui"; +import { Text } from "./text"; type ToastConfig = { title: string; @@ -20,6 +25,7 @@ type ToastConfig = { duration?: number; actionLabel?: string; onAction?: () => void; + variant?: "default" | "success" | "error" | "info"; }; type ToastState = { @@ -30,56 +36,86 @@ type ToastState = { duration: number; actionLabel?: string; onAction?: () => void; + variant?: "default" | "success" | "error" | "info"; }; class ToastManager { - private listeners: Set<(state: ToastState | null) => void> = new Set(); - private currentToast: ToastState | null = null; - private timeoutId: ReturnType | null = null; + private listeners: Set<(state: ToastState[]) => void> = new Set(); + private toasts: ToastState[] = []; + private timeoutIds: Map> = new Map(); + private hoverListeners: Set<(isHovered: boolean) => void> = new Set(); + private isHovered: boolean = false; show(config: ToastConfig) { - if (this.timeoutId) { - clearTimeout(this.timeoutId); - } - const toast: ToastState = { - id: Math.random().toString(36).substr(2, 9), + id: Math.random().toString(36).slice(2, 12), open: true, title: config.title, description: config.description, duration: config.duration ?? 3, actionLabel: config.actionLabel, onAction: config.onAction, + variant: config.variant ?? "default", }; - this.currentToast = toast; + this.toasts = [...this.toasts, toast]; this.notifyListeners(); if (toast.duration > 0) { - this.timeoutId = setTimeout(() => { - this.hide(); + const timeoutId = setTimeout(() => { + this.hide(toast.id); }, toast.duration * 1000); + this.timeoutIds.set(toast.id, timeoutId); } } - hide() { - if (this.timeoutId) { - clearTimeout(this.timeoutId); - this.timeoutId = null; - } - this.currentToast = null; + getToasts() { + return this.toasts; + } + + hide(id: string) { + this.toasts = this.toasts.map((toast) => + toast.id === id ? { ...toast, open: false } : toast, + ); this.notifyListeners(); + + const timeoutId = this.timeoutIds.get(id); + if (timeoutId) { + clearTimeout(timeoutId); + this.timeoutIds.delete(id); + } + + setTimeout(() => { + this.toasts = this.toasts.filter((toast) => toast.id !== id); + this.notifyListeners(); + }, 500); } - subscribe(listener: (state: ToastState | null) => void) { + subscribe(listener: (state: ToastState[]) => void) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; } + subscribeHover(listener: (isHovered: boolean) => void) { + this.hoverListeners.add(listener); + return () => { + this.hoverListeners.delete(listener); + }; + } + + setHovered(hovered: boolean) { + this.isHovered = hovered; + this.notifyHoverListeners(); + } + private notifyListeners() { - this.listeners.forEach((listener) => listener(this.currentToast)); + this.listeners.forEach((listener) => listener(this.toasts)); + } + + private notifyHoverListeners() { + this.hoverListeners.forEach((listener) => listener(this.isHovered)); } } @@ -93,6 +129,7 @@ export const toast = { duration?: number; actionLabel?: string; onAction?: () => void; + variant?: "default" | "success" | "error" | "info"; }, ) => { toastManager.show({ @@ -101,82 +138,158 @@ export const toast = { ...options, }); }, - hide: () => toastManager.hide(), + hide: (id?: string) => { + if (id) { + toastManager.hide(id); + } else { + const toasts = toastManager.getToasts(); + if (toasts.length > 0) { + toastManager.hide(toasts[toasts.length - 1].id); + } + } + }, }; export function useToast() { - const [toastState, setToastState] = useState(null); + const [toasts, setToasts] = useState([]); useEffect(() => { - return toastManager.subscribe(setToastState); + return toastManager.subscribe(setToasts); }, []); return { - toast: toastState, + toasts, ...toast, }; } export function ToastProvider() { - const [toastState, setToastState] = useState(null); + const [toasts, setToasts] = useState([]); useEffect(() => { - return toastManager.subscribe(setToastState); + return toastManager.subscribe(setToasts); }, []); - if (!toastState?.open) return null; - return ( - { - if (!open) toastManager.hide(); - }} - title={toastState.title} - description={toastState.description} - actionLabel={toastState.actionLabel} - onAction={toastState.onAction} - duration={toastState.duration} - /> + <> + {toasts + .slice(-4) + .reverse() + .map((toastState, index) => ( + { + if (!open) toastManager.hide(toastState.id); + }} + title={toastState.title} + description={toastState.description} + actionLabel={toastState.actionLabel} + onAction={toastState.onAction} + duration={toastState.duration} + index={index} + isLatest={index === 0} + totalToasts={toasts.length} + variant={toastState.variant} + /> + ))} + ); } type ToastProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; + title?: string; description?: string; actionLabel?: string; onAction?: () => void; - duration?: number; // seconds + duration?: number; + index?: number; + isLatest?: boolean; + totalToasts?: number; + variant?: "default" | "success" | "error" | "info"; }; export function Toast({ - open, - onOpenChange, - title, + open = false, + onOpenChange = () => {}, + title = "", description, actionLabel = "Action", onAction, - duration = 3, + duration = 60, + index = 0, + isLatest = true, + totalToasts = 0, + variant = "default", }: ToastProps) { const [seconds, setSeconds] = useState(duration); + const [isHovered, setIsHovered] = useState(false); + const intervalRef = useRef | null>(null); + const [height, setHeight] = useState(100); const insets = useSafeAreaInsets(); const { theme } = useTheme(); - const [fadeAnim] = useState(new Animated.Value(0)); - const { width } = useWindowDimensions(); const isWeb = Platform.OS === "web"; + const { width } = useWindowDimensions(); const isDesktop = isWeb && width >= 768; + const opacity = useSharedValue(0); + const translateY = useSharedValue(100); + const scale = useSharedValue(1 - index * 0.05); + + useEffect(() => { + return toastManager.subscribeHover(setIsHovered); + }, []); + + useEffect(() => { + if (isHovered) { + // Stack vertically with proper spacing when hovered + const spacing = height + 20; // Height of each toast + gap + translateY.value = withTiming(-index * spacing, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + scale.value = withTiming(1, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + opacity.value = withTiming(1, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + } else { + // Compact stacked view when not hovered + translateY.value = withTiming(index * 10, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + scale.value = withTiming(1 - index * 0.05, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + opacity.value = withTiming(isLatest ? 1 : 0.8, { + duration: 300, + easing: Easing.out(Easing.exp), + }); + } + }, [isHovered, index, isLatest]); + + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: opacity.value, + transform: [{ translateY: translateY.value }, { scale: scale.value }], + zIndex: 1000 - index, + }; + }); + const containerPosition: ViewStyle = isDesktop ? { - top: undefined, bottom: theme.spacing[4], - right: theme.spacing[4], // <-- use spacing, not 1 + right: theme.spacing[4], alignItems: "flex-end", minWidth: 400, width: 400, - // Do NOT set left at all } : { bottom: insets.bottom + theme.spacing[1], @@ -184,60 +297,92 @@ export function Toast({ right: 0, alignItems: "center", width: "100%", - maxWidth: undefined, }; useEffect(() => { - let interval: ReturnType | null = null; - - if (open) { - setSeconds(duration); - Animated.timing(fadeAnim, { - toValue: 1, - duration: 200, - useNativeDriver: true, - }).start(); - - interval = setInterval(() => { + if (open && !isHovered) { + // Start or resume the timer + intervalRef.current = setInterval(() => { setSeconds((prev) => { if (prev <= 1) { - onOpenChange(false); - if (interval) clearInterval(interval); + runOnJS(onOpenChange)(false); + if (intervalRef.current) clearInterval(intervalRef.current); return duration; } return prev - 1; }); }, 1000); } else { - if (interval) clearInterval(interval); - Animated.timing(fadeAnim, { - toValue: 0, - duration: 150, - useNativeDriver: true, - }).start(); - setSeconds(duration); + // Pause timer when hovered or not open + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } + + if (!open) { + opacity.value = withTiming(0, { duration: 150 }); + translateY.value = withTiming(100, { duration: 150 }); } return () => { - if (interval) clearInterval(interval); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } }; - // eslint-disable-next-line - }, [open, duration]); + }, [open, isHovered]); - if (!open) return null; + useEffect(() => { + if (open) { + setSeconds(duration); + + opacity.value = withTiming(isLatest ? 1 : 0.8, { + duration: 200, + easing: Easing.out(Easing.exp), + }); + translateY.value = withTiming(index * 10, { + duration: 200, + easing: Easing.out(Easing.exp), + }); + scale.value = withTiming(1 - index * 0.05, { + duration: 250, + easing: Easing.out(Easing.exp), + }); + } + }, [open, duration, index]); + + const variantStyles = { + default: { + backgroundColor: theme.colors.secondary, + borderColor: theme.colors.border, + }, + success: { + backgroundColor: theme.colors.success, + borderColor: theme.colors.success, + }, + error: { + backgroundColor: theme.colors.destructive, + borderColor: theme.colors.destructive, + }, + info: { + backgroundColor: theme.colors.info, + borderColor: theme.colors.info, + }, + }; return ( - + setHeight(l.nativeEvent.layout.height)} + style={[styles.container, containerPosition, animatedStyle]} > - isWeb && toastManager.setHovered(true)} + onHoverOut={() => isWeb && toastManager.setHovered(false)} style={[ styles.toast, { - backgroundColor: theme.colors.secondary, - borderColor: theme.colors.border, borderRadius: theme.borderRadius.xl, flexDirection: "column", justifyContent: "space-between", @@ -245,26 +390,26 @@ export function Toast({ padding: theme.spacing[4], width: isDesktop ? "100%" : "95%", }, + variantStyles[variant], ]} > {title} {description ? ( - + {description} ) : null} + Close - +
); diff --git a/js/components/src/lib/theme/theme.tsx b/js/components/src/lib/theme/theme.tsx index 5eae225a..563b5309 100644 --- a/js/components/src/lib/theme/theme.tsx +++ b/js/components/src/lib/theme/theme.tsx @@ -84,6 +84,10 @@ export interface Theme { warning: string; warningForeground: string; + // Info colors + info: string; + infoForeground: string; + // Border and input colors border: string; input: string; @@ -356,6 +360,9 @@ function generateThemeColorsFromPalette( Platform.OS === "ios" ? colors.ios.systemOrange : colors.warning[500], warningForeground: colors.white, + info: Platform.OS === "ios" ? colors.ios.systemTeal : colors.teal[500], + infoForeground: isDark ? palette[50] : palette[900], + border: isDark ? palette[500] + "30" : palette[200] + "30", input: isDark ? palette[800] : palette[200], ring: Platform.OS === "ios" ? colors.ios.systemBlue : colors.primary[500], -- 2.51.2 From 4d7b182d9966a58eaf4429222c5667e41368974e Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 20:02:31 -0500 Subject: [PATCH 120/525] tweak toast anims --- js/components/src/components/ui/toast.tsx | 414 +++++++++++++--------- 1 file changed, 253 insertions(+), 161 deletions(-) diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index bf99b42f..dcd14ecd 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -1,4 +1,5 @@ import { Portal } from "@rn-primitives/portal"; +import { X } from "lucide-react-native"; import { useEffect, useRef, useState } from "react"; import { Platform, @@ -8,14 +9,18 @@ import { View, ViewStyle, } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { + cancelAnimation, Easing, runOnJS, + useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Circle, Svg } from "react-native-svg"; import { useTheme } from "../../ui"; import { Text } from "./text"; @@ -42,7 +47,7 @@ type ToastState = { class ToastManager { private listeners: Set<(state: ToastState[]) => void> = new Set(); private toasts: ToastState[] = []; - private timeoutIds: Map> = new Map(); + private hoverListeners: Set<(isHovered: boolean) => void> = new Set(); private isHovered: boolean = false; @@ -60,13 +65,6 @@ class ToastManager { this.toasts = [...this.toasts, toast]; this.notifyListeners(); - - if (toast.duration > 0) { - const timeoutId = setTimeout(() => { - this.hide(toast.id); - }, toast.duration * 1000); - this.timeoutIds.set(toast.id, timeoutId); - } } getToasts() { @@ -79,12 +77,6 @@ class ToastManager { ); this.notifyListeners(); - const timeoutId = this.timeoutIds.get(id); - if (timeoutId) { - clearTimeout(timeoutId); - this.timeoutIds.delete(id); - } - setTimeout(() => { this.toasts = this.toasts.filter((toast) => toast.id !== id); this.notifyListeners(); @@ -165,35 +157,77 @@ export function useToast() { export function ToastProvider() { const [toasts, setToasts] = useState([]); + const insets = useSafeAreaInsets(); + const { theme } = useTheme(); + const isWeb = Platform.OS === "web"; + const { width } = useWindowDimensions(); + const isDesktop = isWeb && width >= 768; useEffect(() => { return toastManager.subscribe(setToasts); }, []); + const gesture = + Platform.OS === "web" + ? Gesture.Hover() + .onStart(() => { + runOnJS(toastManager.setHovered)(true); + }) + .onEnd(() => { + runOnJS(toastManager.setHovered)(false); + }) + : Gesture.LongPress() + .onStart(() => { + runOnJS(toastManager.setHovered)(true); + }) + .onEnd(() => { + runOnJS(toastManager.setHovered)(false); + }); + + const containerPosition: ViewStyle = isDesktop + ? { + bottom: theme.spacing[4], + right: theme.spacing[4], + alignItems: "flex-end", + minWidth: 400, + width: 400, + } + : { + bottom: insets.bottom + theme.spacing[1], + left: 0, + right: 0, + alignItems: "center", + width: "100%", + }; + return ( - <> - {toasts - .slice(-4) - .reverse() - .map((toastState, index) => ( - { - if (!open) toastManager.hide(toastState.id); - }} - title={toastState.title} - description={toastState.description} - actionLabel={toastState.actionLabel} - onAction={toastState.onAction} - duration={toastState.duration} - index={index} - isLatest={index === 0} - totalToasts={toasts.length} - variant={toastState.variant} - /> - ))} - + + + + {toasts + .slice(-4) + .reverse() + .map((toastState, index) => ( + { + if (!open) toastManager.hide(toastState.id); + }} + title={toastState.title} + description={toastState.description} + actionLabel={toastState.actionLabel} + onAction={toastState.onAction} + duration={toastState.duration} + index={index} + isLatest={index === 0} + totalToasts={toasts.length} + variant={toastState.variant} + /> + ))} + + + ); } @@ -211,6 +245,8 @@ type ToastProps = { variant?: "default" | "success" | "error" | "info"; }; +const AnimatedCircle = Animated.createAnimatedComponent(Circle); + export function Toast({ open = false, onOpenChange = () => {}, @@ -224,16 +260,25 @@ export function Toast({ totalToasts = 0, variant = "default", }: ToastProps) { - const [seconds, setSeconds] = useState(duration); const [isHovered, setIsHovered] = useState(false); - const intervalRef = useRef | null>(null); + const progress = useSharedValue(1); + const remainingTime = useSharedValue(duration * 1000); + const wasOpen = useRef(open); const [height, setHeight] = useState(100); - const insets = useSafeAreaInsets(); const { theme } = useTheme(); const isWeb = Platform.OS === "web"; const { width } = useWindowDimensions(); const isDesktop = isWeb && width >= 768; + const RADIUS = 8; + const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + + const animatedCircleProps = useAnimatedProps(() => { + return { + strokeDashoffset: CIRCUMFERENCE * (1 - progress.value), + }; + }); + const opacity = useSharedValue(0); const translateY = useSharedValue(100); const scale = useSharedValue(1 - index * 0.05); @@ -243,114 +288,87 @@ export function Toast({ }, []); useEffect(() => { + if (open && !wasOpen.current) { + // Toast just opened + progress.value = 1; + remainingTime.value = duration * 1000; + } + wasOpen.current = open; + + if (!open) { + // Close animation + opacity.value = withTiming(0, { duration: 250 }); + translateY.value = withTiming(100, { duration: 250 }); + return; + } + if (isHovered) { // Stack vertically with proper spacing when hovered const spacing = height + 20; // Height of each toast + gap translateY.value = withTiming(-index * spacing, { - duration: 300, + duration: 750, easing: Easing.out(Easing.exp), }); scale.value = withTiming(1, { - duration: 300, + duration: 750, easing: Easing.out(Easing.exp), }); opacity.value = withTiming(1, { - duration: 300, + duration: 750, easing: Easing.out(Easing.exp), }); } else { // Compact stacked view when not hovered - translateY.value = withTiming(index * 10, { - duration: 300, + translateY.value = withTiming(-index * 15, { + duration: 750, easing: Easing.out(Easing.exp), }); - scale.value = withTiming(1 - index * 0.05, { - duration: 300, + scale.value = withTiming(1 - index * 0.1, { + duration: 750, easing: Easing.out(Easing.exp), }); opacity.value = withTiming(isLatest ? 1 : 0.8, { - duration: 300, + duration: 750, easing: Easing.out(Easing.exp), }); } - }, [isHovered, index, isLatest]); - - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - transform: [{ translateY: translateY.value }, { scale: scale.value }], - zIndex: 1000 - index, - }; - }); - - const containerPosition: ViewStyle = isDesktop - ? { - bottom: theme.spacing[4], - right: theme.spacing[4], - alignItems: "flex-end", - minWidth: 400, - width: 400, - } - : { - bottom: insets.bottom + theme.spacing[1], - left: 0, - right: 0, - alignItems: "center", - width: "100%", - }; + }, [open, isHovered, index, isLatest, height, duration]); useEffect(() => { - if (open && !isHovered) { - // Start or resume the timer - intervalRef.current = setInterval(() => { - setSeconds((prev) => { - if (prev <= 1) { - runOnJS(onOpenChange)(false); - if (intervalRef.current) clearInterval(intervalRef.current); - return duration; - } - return prev - 1; - }); - }, 1000); - } else { - // Pause timer when hovered or not open - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; + if (open && isLatest && duration > 0) { + if (isHovered) { + cancelAnimation(progress); + remainingTime.value = progress.value * duration * 1000; + } else { + progress.value = withTiming( + 0, + { + duration: remainingTime.value, + easing: Easing.linear, + }, + (finished) => { + if (finished) { + runOnJS(onOpenChange)(false); + } + }, + ); } - } - - if (!open) { - opacity.value = withTiming(0, { duration: 150 }); - translateY.value = withTiming(100, { duration: 150 }); + } else { + cancelAnimation(progress); } return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + cancelAnimation(progress); }; - }, [open, isHovered]); + }, [open, isLatest, isHovered, duration]); - useEffect(() => { - if (open) { - setSeconds(duration); - - opacity.value = withTiming(isLatest ? 1 : 0.8, { - duration: 200, - easing: Easing.out(Easing.exp), - }); - translateY.value = withTiming(index * 10, { - duration: 200, - easing: Easing.out(Easing.exp), - }); - scale.value = withTiming(1 - index * 0.05, { - duration: 250, - easing: Easing.out(Easing.exp), - }); - } - }, [open, duration, index]); + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: opacity.value, + transform: [{ translateY: translateY.value }, { scale: scale.value }], + zIndex: 1000 - index, + }; + }); const variantStyles = { default: { @@ -372,28 +390,34 @@ export function Toast({ }; return ( - - setHeight(l.nativeEvent.layout.height)} - style={[styles.container, containerPosition, animatedStyle]} + setHeight(l.nativeEvent.layout.height)} + style={[styles.container, animatedStyle]} + > + - isWeb && toastManager.setHovered(true)} - onHoverOut={() => isWeb && toastManager.setHovered(false)} - style={[ - styles.toast, - { - borderRadius: theme.borderRadius.xl, - flexDirection: "column", - justifyContent: "space-between", - alignItems: "center", - padding: theme.spacing[4], - width: isDesktop ? "100%" : "95%", - }, - variantStyles[variant], - ]} + - + ) : null} - + {isLatest && duration > 0 ? ( + onOpenChange(false)} + style={{ + alignItems: "center", + justifyContent: "center", + }} + > + + + + + {!onAction && ( + + + + )} + + ) : ( + onOpenChange(false)} + style={{ + alignItems: "center", + justifyContent: "center", + }} + > + + + + {!onAction && ( + + + + )} + + )} + + {onAction && ( - {onAction && ( - - - {actionLabel} - - - )} + + + {actionLabel} + + Close - - - + )} + + ); } const styles = StyleSheet.create({ - container: { + providerContainer: { position: "absolute", zIndex: 1000, - paddingHorizontal: 16, + }, + container: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + alignItems: "center", }, toast: { opacity: 0.95, -- 2.51.2 From d90083bd9285b2db61c84c6bde6685d3f750dc7b Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 20:12:23 -0500 Subject: [PATCH 121/525] yeah --- js/app/src/router.tsx | 47 ++++++++--------------- js/components/src/components/ui/toast.tsx | 8 ++++ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/js/app/src/router.tsx b/js/app/src/router.tsx index 1e9e3c47..554daf8d 100644 --- a/js/app/src/router.tsx +++ b/js/app/src/router.tsx @@ -15,11 +15,10 @@ import { useRoute, } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { Text, useTheme } from "@streamplace/components"; +import { Text, useTheme, useToast } from "@streamplace/components"; import { Provider, Settings } from "components"; import AQLink from "components/aqlink"; import Login from "components/login/login"; -import Popup from "components/popup"; import Sidebar, { ExternalDrawerItem } from "components/sidebar/sidebar"; import * as ExpoLinking from "expo-linking"; import { hydrate, selectHydrated } from "features/base/baseSlice"; @@ -337,6 +336,8 @@ export function StreamplaceDrawer() { const sidebar = useSidebarControl(); + const toast = useToast(); + SystemBars.setStyle("dark"); // Top-level stuff to handle push notification registration @@ -397,6 +398,19 @@ export function StreamplaceDrawer() { if (!hydrated) { return ; } + + if (isWeb && livePopup) { + toast.show("You are live!", "Do you want to go to the Live Dashboard?", { + actionLabel: "Go", + onAction: () => { + navigation.navigate("LiveDashboard"); + setLivePopup(false); + }, + onClose: () => setLivePopup(false), + variant: "error", + }); + } + return ( <> @@ -589,35 +603,6 @@ export function StreamplaceDrawer() { }} /> - {isWeb && livePopup && ( - { - navigation.navigate("LiveDashboard"); - setLivePopup(false); - }} - onClose={() => { - setLivePopup(false); - }} - containerProps={{ - style: { bottom: 32 }, - }} - bubbleProps={{ - style: { backgroundColor: "#cc0000" }, - }} - > - - ✨YOU ARE LIVE!!!✨ - - - {isNative ? "Tap" : "Click"} here to go to the live dashboard - - - )} ); } diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index dcd14ecd..4b28d833 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -30,6 +30,7 @@ type ToastConfig = { duration?: number; actionLabel?: string; onAction?: () => void; + onClose?: () => void; variant?: "default" | "success" | "error" | "info"; }; @@ -41,6 +42,7 @@ type ToastState = { duration: number; actionLabel?: string; onAction?: () => void; + onClose?: () => void; variant?: "default" | "success" | "error" | "info"; }; @@ -60,6 +62,7 @@ class ToastManager { duration: config.duration ?? 3, actionLabel: config.actionLabel, onAction: config.onAction, + onClose: config.onClose, variant: config.variant ?? "default", }; @@ -72,6 +75,10 @@ class ToastManager { } hide(id: string) { + const toast = this.toasts.find((t) => t.id === id); + if (toast?.onClose) { + toast.onClose(); + } this.toasts = this.toasts.map((toast) => toast.id === id ? { ...toast, open: false } : toast, ); @@ -121,6 +128,7 @@ export const toast = { duration?: number; actionLabel?: string; onAction?: () => void; + onClose?: () => void; variant?: "default" | "success" | "error" | "info"; }, ) => { -- 2.51.2 From e6497dc5ad33700f3add49f5a21207ffff5a15ab Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 20:14:45 -0500 Subject: [PATCH 122/525] add showManual --- js/components/src/components/ui/toast.tsx | 291 ++++++++++++---------- 1 file changed, 166 insertions(+), 125 deletions(-) diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index 4b28d833..6ff8358d 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -25,25 +25,33 @@ import { useTheme } from "../../ui"; import { Text } from "./text"; type ToastConfig = { - title: string; + title?: string; description?: string; duration?: number; actionLabel?: string; onAction?: () => void; onClose?: () => void; variant?: "default" | "success" | "error" | "info"; + render?: (props: { + close: () => void; + action?: () => void; + }) => React.ReactNode; }; type ToastState = { id: string; open: boolean; - title: string; + title?: string; description?: string; duration: number; actionLabel?: string; onAction?: () => void; onClose?: () => void; variant?: "default" | "success" | "error" | "info"; + render?: (props: { + close: () => void; + action?: () => void; + }) => React.ReactNode; }; class ToastManager { @@ -64,6 +72,7 @@ class ToastManager { onAction: config.onAction, onClose: config.onClose, variant: config.variant ?? "default", + render: config.render, }; this.toasts = [...this.toasts, toast]; @@ -148,6 +157,24 @@ export const toast = { } } }, + showManual: ( + render: (props: { + close: () => void; + action?: () => void; + }) => React.ReactNode, + options?: { + duration?: number; + actionLabel?: string; + onAction?: () => void; + onClose?: () => void; + variant?: "default" | "success" | "error" | "info"; + }, + ) => { + toastManager.show({ + render, + ...options, + }); + }, }; export function useToast() { @@ -231,6 +258,7 @@ export function ToastProvider() { isLatest={index === 0} totalToasts={toasts.length} variant={toastState.variant} + render={toastState.render} /> ))} @@ -251,6 +279,10 @@ type ToastProps = { isLatest?: boolean; totalToasts?: number; variant?: "default" | "success" | "error" | "info"; + render?: (props: { + close: () => void; + action?: () => void; + }) => React.ReactNode; }; const AnimatedCircle = Animated.createAnimatedComponent(Circle); @@ -267,6 +299,7 @@ export function Toast({ isLatest = true, totalToasts = 0, variant = "default", + render, }: ToastProps) { const [isHovered, setIsHovered] = useState(false); const progress = useSharedValue(1); @@ -416,133 +449,141 @@ export function Toast({ variantStyles[variant], ]} > - - - onOpenChange(false), action: onAction }) + ) : ( + <> + - {title} - - {description ? ( - - {description} - - ) : null} - - {isLatest && duration > 0 ? ( - onOpenChange(false)} - style={{ - alignItems: "center", - justifyContent: "center", - }} - > - - - - - {!onAction && ( - - - + + + {title} + + {description ? ( + + {description} + + ) : null} + + {isLatest && duration > 0 ? ( + onOpenChange(false)} + style={{ + alignItems: "center", + justifyContent: "center", + }} + > + + + + + {!onAction && ( + + + + )} + + ) : ( + onOpenChange(false)} + style={{ + alignItems: "center", + justifyContent: "center", + }} + > + + + + {!onAction && ( + + + + )} + )} - - ) : ( - onOpenChange(false)} - style={{ - alignItems: "center", - justifyContent: "center", - }} - > - - - - {!onAction && ( - - - - )} - - )} - - {onAction && ( - - - - {actionLabel} - - - onOpenChange(false)} - > - Close - - + + {onAction && ( + + + + {actionLabel} + + + onOpenChange(false)} + > + Close + + + )} + )} -- 2.51.2 From 780fd89fe1e5ec96185a325f65bec8445a8eb339 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 21:58:59 -0500 Subject: [PATCH 123/525] add positioning --- js/components/src/components/ui/toast.tsx | 365 +++++++++++++++------- js/components/src/lib/theme/theme.tsx | 11 +- 2 files changed, 249 insertions(+), 127 deletions(-) diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index 6ff8358d..b09751b2 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -22,8 +22,18 @@ import Animated, { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Circle, Svg } from "react-native-svg"; import { useTheme } from "../../ui"; +import { Button } from "./button"; import { Text } from "./text"; +type Position = + | "auto" + | "top-left" + | "top-center" + | "top-right" + | "bottom-left" + | "bottom-center" + | "bottom-right"; + type ToastConfig = { title?: string; description?: string; @@ -36,6 +46,7 @@ type ToastConfig = { close: () => void; action?: () => void; }) => React.ReactNode; + position?: Position; }; type ToastState = { @@ -52,6 +63,7 @@ type ToastState = { close: () => void; action?: () => void; }) => React.ReactNode; + position: Position; }; class ToastManager { @@ -73,6 +85,7 @@ class ToastManager { onClose: config.onClose, variant: config.variant ?? "default", render: config.render, + position: config.position ?? "auto", }; this.toasts = [...this.toasts, toast]; @@ -139,6 +152,7 @@ export const toast = { onAction?: () => void; onClose?: () => void; variant?: "default" | "success" | "error" | "info"; + position?: Position; }, ) => { toastManager.show({ @@ -168,6 +182,7 @@ export const toast = { onAction?: () => void; onClose?: () => void; variant?: "default" | "success" | "error" | "info"; + position?: Position; }, ) => { toastManager.show({ @@ -192,97 +207,211 @@ export function useToast() { export function ToastProvider() { const [toasts, setToasts] = useState([]); + + useEffect(() => { + return toastManager.subscribe(setToasts); + }, []); + + const toastsByPosition = toasts.reduce( + (acc, toast) => { + const { position } = toast; + if (!acc[position]) { + acc[position] = []; + } + acc[position].push(toast); + return acc; + }, + {} as Record, + ); + + return ( + <> + {Object.entries(toastsByPosition).map(([position, toasts]) => ( + + ))} + + ); +} + +function ToastContainer({ + position = "auto", + toasts, +}: { + position?: Position; + toasts: ToastState[]; +}) { const insets = useSafeAreaInsets(); const { theme } = useTheme(); const isWeb = Platform.OS === "web"; const { width } = useWindowDimensions(); const isDesktop = isWeb && width >= 768; + const isTop = position.includes("top"); + const visibleToasts = toasts.slice(-4); + const prevToastIds = useRef(visibleToasts.map((t) => t.id)); + const allKnownToastIds = useRef>( + new Set(toasts.map((t) => t.id)), + ); + const [isHovered, setIsHovered] = useState(false); useEffect(() => { - return toastManager.subscribe(setToasts); + return toastManager.subscribeHover(setIsHovered); }, []); + useEffect(() => { + const currentIds = visibleToasts.map((t) => t.id); + const hasNewToast = currentIds.some( + (id) => !allKnownToastIds.current.has(id), + ); + + if (hasNewToast && isHovered) { + // Brand new toast arrived while expanded - collapse momentarily + toastManager.setHovered(false); + setTimeout(() => { + toastManager.setHovered(true); + }, 700); + } else if (isHovered) { + toastManager.setHovered(true); + setTimeout(() => { + toastManager.setHovered(true); + }, 700); + } + + // Update known toast IDs + toasts.forEach((t) => allKnownToastIds.current.add(t.id)); + prevToastIds.current = currentIds; + }, [visibleToasts, isHovered, toasts]); + + const setHovered = (value: boolean) => toastManager.setHovered(value); + + const pan = Gesture.Pan() + .onUpdate((event) => { + const velocity = isTop ? -event.velocityY : event.velocityY; + if (velocity > 500) { + runOnJS(setHovered)(true); + } else if (velocity < -500) { + runOnJS(setHovered)(false); + } + }) + .onEnd((event) => { + const translationY = isTop ? -event.translationY : event.translationY; + if (translationY > 50) { + runOnJS(setHovered)(true); + } else if (translationY < -50) { + runOnJS(setHovered)(false); + } + }); + const gesture = Platform.OS === "web" ? Gesture.Hover() .onStart(() => { - runOnJS(toastManager.setHovered)(true); + runOnJS(setHovered)(true); }) .onEnd(() => { - runOnJS(toastManager.setHovered)(false); - }) - : Gesture.LongPress() - .onStart(() => { - runOnJS(toastManager.setHovered)(true); + runOnJS(setHovered)(false); }) - .onEnd(() => { - runOnJS(toastManager.setHovered)(false); - }); - - const containerPosition: ViewStyle = isDesktop - ? { - bottom: theme.spacing[4], - right: theme.spacing[4], - alignItems: "flex-end", - minWidth: 400, - width: 400, - } - : { - bottom: insets.bottom + theme.spacing[1], - left: 0, - right: 0, - alignItems: "center", - width: "100%", - }; + : pan; + + const getPositionStyle = (): ViewStyle => { + const resolvedPosition = + position === "auto" + ? isDesktop + ? "bottom-right" + : "top-center" + : position; + + const styles: ViewStyle = { + position: "absolute", + zIndex: 1000, + width: isDesktop ? 400 : "100%", + paddingHorizontal: isDesktop ? 0 : theme.spacing[4], + }; + + if (resolvedPosition.includes("top")) { + styles.top = insets.top + theme.spacing[4]; + } + if (resolvedPosition.includes("bottom")) { + styles.bottom = insets.bottom + theme.spacing[4]; + } + if (resolvedPosition.includes("left")) { + styles.left = insets.left + theme.spacing[4]; + styles.alignItems = "flex-start"; + } + if (resolvedPosition.includes("right")) { + styles.right = insets.right + theme.spacing[4]; + styles.alignItems = "flex-end"; + } + if (resolvedPosition.includes("center")) { + styles.left = 0; + styles.right = 0; + styles.alignItems = "center"; + } + + return styles; + }; return ( - - {toasts - .slice(-4) - .reverse() - .map((toastState, index) => ( - { - if (!open) toastManager.hide(toastState.id); - }} - title={toastState.title} - description={toastState.description} - actionLabel={toastState.actionLabel} - onAction={toastState.onAction} - duration={toastState.duration} - index={index} - isLatest={index === 0} - totalToasts={toasts.length} - variant={toastState.variant} - render={toastState.render} - /> - ))} + + {visibleToasts.reverse().map((toastState, index) => ( + { + if (!open) toastManager.hide(toastState.id); + }} + index={index} + isLatest={index === 0} + totalToasts={toasts.length} + position={ + position === "auto" + ? isDesktop + ? "bottom-right" + : "top-center" + : position + } + /> + ))} ); } -type ToastProps = { +export function AndMore({ more }: { more: number }) { + const { theme } = useTheme(); + return ( + + + and {more} more notification + {more === 1 ? "" : "s"} + + + ); +} + +type PartialBy = Omit & Partial>; + +type ToastProps = PartialBy, "position"> & { open?: boolean; onOpenChange?: (open: boolean) => void; - title?: string; - description?: string; - actionLabel?: string; - onAction?: () => void; - duration?: number; index?: number; isLatest?: boolean; totalToasts?: number; - variant?: "default" | "success" | "error" | "info"; - render?: (props: { - close: () => void; - action?: () => void; - }) => React.ReactNode; }; const AnimatedCircle = Animated.createAnimatedComponent(Circle); @@ -300,6 +429,7 @@ export function Toast({ totalToasts = 0, variant = "default", render, + position = "auto", }: ToastProps) { const [isHovered, setIsHovered] = useState(false); const progress = useSharedValue(1); @@ -311,7 +441,11 @@ export function Toast({ const { width } = useWindowDimensions(); const isDesktop = isWeb && width >= 768; - const RADIUS = 8; + const { top, bottom } = useSafeAreaInsets(); + + const isTop = position.includes("top"); + + const RADIUS = 12; const CIRCUMFERENCE = 2 * Math.PI * RADIUS; const animatedCircleProps = useAnimatedProps(() => { @@ -321,7 +455,7 @@ export function Toast({ }); const opacity = useSharedValue(0); - const translateY = useSharedValue(100); + const translateY = useSharedValue(isTop ? -100 : 100); const scale = useSharedValue(1 - index * 0.05); useEffect(() => { @@ -339,14 +473,14 @@ export function Toast({ if (!open) { // Close animation opacity.value = withTiming(0, { duration: 250 }); - translateY.value = withTiming(100, { duration: 250 }); + translateY.value = withTiming(isTop ? -100 : 100, { duration: 250 }); return; } if (isHovered) { // Stack vertically with proper spacing when hovered - const spacing = height + 20; // Height of each toast + gap - translateY.value = withTiming(-index * spacing, { + const spacing = height + 8; // Height of each toast + gap + translateY.value = withTiming((isTop ? index : -index) * spacing, { duration: 750, easing: Easing.out(Easing.exp), }); @@ -360,7 +494,7 @@ export function Toast({ }); } else { // Compact stacked view when not hovered - translateY.value = withTiming(-index * 15, { + translateY.value = withTiming((isTop ? index : -index) * 15, { duration: 750, easing: Easing.out(Easing.exp), }); @@ -368,7 +502,7 @@ export function Toast({ duration: 750, easing: Easing.out(Easing.exp), }); - opacity.value = withTiming(isLatest ? 1 : 0.8, { + opacity.value = withTiming(isLatest ? 1 : 0.95, { duration: 750, easing: Easing.out(Easing.exp), }); @@ -406,7 +540,11 @@ export function Toast({ const animatedStyle = useAnimatedStyle(() => { return { opacity: opacity.value, - transform: [{ translateY: translateY.value }, { scale: scale.value }], + transform: [ + // +22 is to get it just below the header + { translateY: translateY.value + (isTop ? top / 2 : -bottom) }, + { scale: scale.value }, + ], zIndex: 1000 - index, }; }); @@ -430,10 +568,20 @@ export function Toast({ }, }; + const buttonTypeMap = { + default: "primary", + success: "success", + error: "primary", + info: "secondary", + } as const; + return ( setHeight(l.nativeEvent.layout.height)} - style={[styles.container, animatedStyle]} + style={[ + isTop ? styles.containerTop : styles.containerBottom, + animatedStyle, + ]} > - - - {title} - - {description ? ( - - {description} - - ) : null} + + {title} + {description ? {description} : null} {isLatest && duration > 0 ? ( - + @@ -538,7 +676,7 @@ export function Toast({ {!onAction && ( - + )} @@ -553,34 +691,14 @@ export function Toast({ width: "100%", }} > - + + )} @@ -595,15 +713,22 @@ const styles = StyleSheet.create({ position: "absolute", zIndex: 1000, }, - container: { + containerBottom: { position: "absolute", bottom: 0, left: 0, right: 0, alignItems: "center", }, + containerTop: { + position: "absolute", + top: 0, + left: 0, + right: 0, + alignItems: "center", + }, toast: { - opacity: 0.95, + opacity: 0.99, borderWidth: 1, gap: 8, }, diff --git a/js/components/src/lib/theme/theme.tsx b/js/components/src/lib/theme/theme.tsx index 563b5309..dd7c938d 100644 --- a/js/components/src/lib/theme/theme.tsx +++ b/js/components/src/lib/theme/theme.tsx @@ -348,19 +348,16 @@ function generateThemeColorsFromPalette( accent: isDark ? palette[800] : palette[100], accentForeground: isDark ? palette[50] : palette[900], - destructive: - Platform.OS === "ios" ? colors.ios.systemRed : colors.destructive[500], + destructive: colors.destructive[800], destructiveForeground: colors.white, - success: - Platform.OS === "ios" ? colors.ios.systemGreen : colors.success[500], + success: colors.success[800], successForeground: colors.white, - warning: - Platform.OS === "ios" ? colors.ios.systemOrange : colors.warning[500], + warning: colors.warning[800], warningForeground: colors.white, - info: Platform.OS === "ios" ? colors.ios.systemTeal : colors.teal[500], + info: colors.blue[800], infoForeground: isDark ? palette[50] : palette[900], border: isDark ? palette[500] + "30" : palette[200] + "30", -- 2.51.2 From 346d869575629d60b64244d7a6e4a27e10f8e707 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 6 Oct 2025 22:52:09 -0500 Subject: [PATCH 124/525] do variable heights --- js/components/src/components/ui/toast.tsx | 51 +++++++++++++++++------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/js/components/src/components/ui/toast.tsx b/js/components/src/components/ui/toast.tsx index b09751b2..70cdff29 100644 --- a/js/components/src/components/ui/toast.tsx +++ b/js/components/src/components/ui/toast.tsx @@ -69,6 +69,7 @@ type ToastState = { class ToastManager { private listeners: Set<(state: ToastState[]) => void> = new Set(); private toasts: ToastState[] = []; + private toastHeights: Map = new Map(); private hoverListeners: Set<(isHovered: boolean) => void> = new Set(); private isHovered: boolean = false; @@ -131,6 +132,14 @@ class ToastManager { this.notifyHoverListeners(); } + updateToastHeight(id: string, height: number) { + this.toastHeights.set(id, height); + } + + getToastHeight(id: string): number { + return this.toastHeights.get(id) || 100; // Default fallback height + } + private notifyListeners() { this.listeners.forEach((listener) => listener(this.toasts)); } @@ -367,7 +376,6 @@ function ToastContainer({ }} index={index} isLatest={index === 0} - totalToasts={toasts.length} position={ position === "auto" ? isDesktop @@ -407,11 +415,11 @@ export function AndMore({ more }: { more: number }) { type PartialBy = Omit & Partial>; type ToastProps = PartialBy, "position"> & { + id?: string; open?: boolean; onOpenChange?: (open: boolean) => void; index?: number; isLatest?: boolean; - totalToasts?: number; }; const AnimatedCircle = Animated.createAnimatedComponent(Circle); @@ -426,16 +434,16 @@ export function Toast({ duration = 60, index = 0, isLatest = true, - totalToasts = 0, variant = "default", render, position = "auto", + id, }: ToastProps) { const [isHovered, setIsHovered] = useState(false); const progress = useSharedValue(1); const remainingTime = useSharedValue(duration * 1000); const wasOpen = useRef(open); - const [height, setHeight] = useState(100); + const [measuredHeight, setMeasuredHeight] = useState(100); const { theme } = useTheme(); const isWeb = Platform.OS === "web"; const { width } = useWindowDimensions(); @@ -479,11 +487,22 @@ export function Toast({ if (isHovered) { // Stack vertically with proper spacing when hovered - const spacing = height + 8; // Height of each toast + gap - translateY.value = withTiming((isTop ? index : -index) * spacing, { - duration: 750, - easing: Easing.out(Easing.exp), - }); + // Calculate cumulative height of all toasts before this one + let cumulativeHeight = 0; + const allToasts = toastManager.getToasts().slice(-4).reverse(); // Get visible toasts in same order as rendered + for (let i = 0; i < index; i++) { + if (allToasts[i]) { + cumulativeHeight += toastManager.getToastHeight(allToasts[i].id) + 8; // height + gap + } + } + + translateY.value = withTiming( + isTop ? cumulativeHeight : -cumulativeHeight, + { + duration: 750, + easing: Easing.out(Easing.exp), + }, + ); scale.value = withTiming(1, { duration: 750, easing: Easing.out(Easing.exp), @@ -507,7 +526,7 @@ export function Toast({ easing: Easing.out(Easing.exp), }); } - }, [open, isHovered, index, isLatest, height, duration]); + }, [open, isHovered, index, isLatest, measuredHeight, duration, id]); useEffect(() => { if (open && isLatest && duration > 0) { @@ -577,7 +596,13 @@ export function Toast({ return ( setHeight(l.nativeEvent.layout.height)} + onLayout={(l) => { + const height = l.nativeEvent.layout.height; + setMeasuredHeight(height); + if (id) { + toastManager.updateToastHeight(id, height); + } + }} style={[ isTop ? styles.containerTop : styles.containerBottom, animatedStyle, @@ -610,7 +635,7 @@ export function Toast({ gap: theme.spacing[4], }} > - + {title} {description ? {description} : null} @@ -667,7 +692,7 @@ export function Toast({ Date: Tue, 7 Oct 2025 00:08:04 -0500 Subject: [PATCH 125/525] fix home toast? --- js/app/src/router.tsx | 38 ++++++++++++++------------- js/components/src/lib/theme/theme.tsx | 8 +++--- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/js/app/src/router.tsx b/js/app/src/router.tsx index 554daf8d..ef7c25f6 100644 --- a/js/app/src/router.tsx +++ b/js/app/src/router.tsx @@ -331,7 +331,6 @@ export function StreamplaceDrawer() { const { isWeb, isElectron, isNative, isBrowser } = usePlatform(); const navigation = useNavigation(); const dispatch = useAppDispatch(); - const [poppedUp, setPoppedUp] = useState(false); const [livePopup, setLivePopup] = useState(false); const sidebar = useSidebarControl(); @@ -386,31 +385,34 @@ export function StreamplaceDrawer() { let foregroundColor = theme.theme.colors.text || "#fff"; - const [isLiveDashboard, setIsLiveDashboard] = useState(true); + // are we in the live dashboard? + const [isLiveDashboard, setIsLiveDashboard] = useState(false); useEffect(() => { - if (!isLiveDashboard && userIsLive && !poppedUp) { - setPoppedUp(true); - setLivePopup(true); + console.log({ + isLiveDashboard, + userIsLive, + livePopup, + navi: navigation.getState()?.routes, + }); + if (!isLiveDashboard && userIsLive) { + toast.show("You are live!", "Do you want to go to your Live Dashboard?", { + actionLabel: "Go", + onAction: () => { + navigation.navigate("LiveDashboard"); + setLivePopup(false); + }, + onClose: () => setLivePopup(false), + variant: "error", + duration: 8, + }); } - }, [userIsLive, poppedUp]); + }, [userIsLive]); const externalItems = useExternalItems(); if (!hydrated) { return ; } - if (isWeb && livePopup) { - toast.show("You are live!", "Do you want to go to the Live Dashboard?", { - actionLabel: "Go", - onAction: () => { - navigation.navigate("LiveDashboard"); - setLivePopup(false); - }, - onClose: () => setLivePopup(false), - variant: "error", - }); - } - return ( <> diff --git a/js/components/src/lib/theme/theme.tsx b/js/components/src/lib/theme/theme.tsx index dd7c938d..5d941d3f 100644 --- a/js/components/src/lib/theme/theme.tsx +++ b/js/components/src/lib/theme/theme.tsx @@ -348,16 +348,16 @@ function generateThemeColorsFromPalette( accent: isDark ? palette[800] : palette[100], accentForeground: isDark ? palette[50] : palette[900], - destructive: colors.destructive[800], + destructive: colors.destructive[700], destructiveForeground: colors.white, - success: colors.success[800], + success: colors.success[700], successForeground: colors.white, - warning: colors.warning[800], + warning: colors.warning[700], warningForeground: colors.white, - info: colors.blue[800], + info: colors.blue[700], infoForeground: isDark ? palette[50] : palette[900], border: isDark ? palette[500] + "30" : palette[200] + "30", -- 2.51.2 From e6fb6650bc7087fefb3017ebd31bbf324cdd5601 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 7 Oct 2025 13:58:58 +0100 Subject: [PATCH 126/525] iroh: readd iroh-test --- .vscode/settings.json | 2 +- Makefile | 7 +++ pkg/iroh/iroh_test/main.go | 97 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 pkg/iroh/iroh_test/main.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 076ee90f..4b90826e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ "mesonbuild.configureOnOpen": false, "cSpell.words": ["Devplace", "streamplace", "webrtc"], "go.lintTool": "golangci-lint-v2", - "go.lintFlags": ["--path-mode=abs"], + "go.lintFlags": ["--path-mode=abs", "--fast-only"], "go.formatTool": "custom", "go.alternateTools": { "customFormatter": "golangci-lint-v2" diff --git a/Makefile b/Makefile index 2d11b979..9919566c 100644 --- a/Makefile +++ b/Makefile @@ -304,6 +304,13 @@ dev-test: LD_LIBRARY_PATH=$(shell realpath $(BUILDDIR))/lib \ bash -euo pipefail -c "go test -p 1 -timeout 300s ./pkg/... -v | tee /dev/stderr | go-junit-report -out test.xml" +.PHONY: iroh-test +iroh-test: + $(MAKE) dev-rust + PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) \ + LD_LIBRARY_PATH=$(shell realpath $(BUILDDIR))/lib \ + go build -o $(BUILDDIR)/iroh-test ./pkg/iroh/iroh_test/... + # _ _____ _ _ _______ _____ _ _ _____ # | | |_ _| \ | |__ __|_ _| \ | |/ ____| # | | | | | \| | | | | | | \| | | __ diff --git a/pkg/iroh/iroh_test/main.go b/pkg/iroh/iroh_test/main.go new file mode 100644 index 00000000..5724723a --- /dev/null +++ b/pkg/iroh/iroh_test/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "crypto/rand" + "fmt" + "os" + "reflect" + + iroh "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" + _ "stream.place/streamplace/pkg/streamplacedeps" +) + +func isNilError(err error) bool { + if err == nil { + return true + } + + v := reflect.ValueOf(err) + return v.Kind() == reflect.Ptr && v.IsNil() +} + +func panicIfErr(err error) { + if !isNilError(err) { + panic(err) + } +} + +func main() { + tickets := os.Args[1:] + + secret := make([]byte, 32) + _, err := rand.Read(secret) + panicIfErr(err) + + fmt.Println("Starting with tickets", tickets) + config := iroh.Config{ + Key: secret, + Topic: make([]byte, 32), // all zero topic for testing + MaxSendDuration: 1000_000_000, // 1s + } + fmt.Printf("Config created %+v\n", config) + node, err := iroh.NodeSender(config) + panicIfErr(err) + + db := node.Db() + w := node.NodeScope() + + node_id, err := node.NodeId() + panicIfErr(err) + fmt.Println("Node ID:", node_id) + + ticket, err := node.Ticket() + panicIfErr(err) + fmt.Println("Ticket:", ticket) + + if len(tickets) > 0 { + err = node.JoinPeers(tickets) + panicIfErr(err) + } + + err = w.Put(nil, []byte("hello"), []byte("world")) + panicIfErr(err) + stream := []byte("stream1") + err = w.Put(&stream, []byte("subscribed"), []byte("true")) + panicIfErr(err) + + filter := iroh.NewFilter() + items, err := db.IterWithOpts(filter) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items) + + filter2 := iroh.NewFilter().Global() + items2, err := db.IterWithOpts(filter2) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items2) + + filter3 := iroh.NewFilter().Stream(stream) + items3, err := db.IterWithOpts(filter3) + panicIfErr(err) + fmt.Printf("Iter items: %+v\n", items3) + + sub := db.Subscribe(iroh.NewFilter()) + for { + ev, err := sub.NextRaw() + panicIfErr(err) + switch (*ev).(type) { + case iroh.SubscribeItemEntry: + fmt.Printf("%+v\n", (*ev).(iroh.SubscribeItemEntry)) + case iroh.SubscribeItemCurrentDone: + fmt.Printf("Got current done event: %+v\n", (*ev).(iroh.SubscribeItemCurrentDone)) + case iroh.SubscribeItemExpired: + fmt.Printf("Got expired event: %+v\n", (*ev).(iroh.SubscribeItemExpired)) + case iroh.SubscribeItemOther: + fmt.Printf("Got other event: %+v\n", (*ev).(iroh.SubscribeItemOther)) + } + } +} -- 2.51.2 From d83e5c1dd0d0fb270f393600f86b1488938d6027 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 7 Oct 2025 16:19:05 +0300 Subject: [PATCH 127/525] Remove isNilError hack. We don't need this anymore since we are using a forked version of go-uniffi-bindgen that does not emit typed nils. --- pkg/iroh/iroh_test/main.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/iroh/iroh_test/main.go b/pkg/iroh/iroh_test/main.go index 5724723a..7b8361f8 100644 --- a/pkg/iroh/iroh_test/main.go +++ b/pkg/iroh/iroh_test/main.go @@ -10,17 +10,8 @@ import ( _ "stream.place/streamplace/pkg/streamplacedeps" ) -func isNilError(err error) bool { - if err == nil { - return true - } - - v := reflect.ValueOf(err) - return v.Kind() == reflect.Ptr && v.IsNil() -} - func panicIfErr(err error) { - if !isNilError(err) { + if err != nil { panic(err) } } -- 2.51.2 From c5c0e10105b1b68b2551c579e43481e1df1a8ef3 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 7 Oct 2025 17:22:08 +0300 Subject: [PATCH 128/525] Implement shutdown --- Cargo.lock | 3 +- rust/iroh-streamplace/Cargo.toml | 2 +- rust/iroh-streamplace/src/error.rs | 8 +++-- rust/iroh-streamplace/src/node/db.rs | 20 ++++++++++-- rust/iroh-streamplace/src/node/streams.rs | 40 +++++++++++++++++++++-- rust/iroh-streamplace/src/node_addr.rs | 1 - 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f60da078..f0978a74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2493,8 +2493,7 @@ dependencies = [ [[package]] name = "iroh-smol-kv" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf263d8aba7c7ec5baddcec76d9a6881b3f5470d293707b3881545f9548f31c" +source = "git+https://github.com/n0-computer/iroh-smol-kv#4067d52fd16efc922352e072418bc2cec061b6c4" dependencies = [ "bytes", "chrono", diff --git a/rust/iroh-streamplace/Cargo.toml b/rust/iroh-streamplace/Cargo.toml index d67ce3ee..5ed974fa 100644 --- a/rust/iroh-streamplace/Cargo.toml +++ b/rust/iroh-streamplace/Cargo.toml @@ -29,7 +29,7 @@ c2pa = { git = "https://github.com/hyphacoop/c2pa-rs.git", rev = "1b84d40219b273 ] } thiserror = "2.0.16" serde_json = "1.0.145" -iroh-smol-kv = "0.2.0" +iroh-smol-kv = { version = "0.2.0", git = "https://github.com/n0-computer/iroh-smol-kv" } hex = "0.4.3" iroh-gossip = "0.92.0" ref-cast = "1.0.25" diff --git a/rust/iroh-streamplace/src/error.rs b/rust/iroh-streamplace/src/error.rs index 1314ca55..fa047477 100644 --- a/rust/iroh-streamplace/src/error.rs +++ b/rust/iroh-streamplace/src/error.rs @@ -4,12 +4,16 @@ #[snafu(visibility(pub(crate)))] pub enum Error { #[snafu(display("Bind failure"), context(false))] - IrohBind { source: iroh::endpoint::BindError }, + IrohBind { + source: iroh::endpoint::BindError, + }, #[snafu(display("Failed to connect"), context(false))] IrohConnect { source: iroh::endpoint::ConnectError, }, MissingConnection, #[snafu(display("RPC error"), context(false))] - Irpc { source: irpc::Error }, + Irpc { + source: irpc::Error, + }, } diff --git a/rust/iroh-streamplace/src/node/db.rs b/rust/iroh-streamplace/src/node/db.rs index 9323fc96..944f3954 100644 --- a/rust/iroh-streamplace/src/node/db.rs +++ b/rust/iroh-streamplace/src/node/db.rs @@ -12,9 +12,8 @@ use ref_cast::RefCast; use snafu::Snafu; use tokio::sync::Mutex; -use crate::public_key::PublicKey; - use super::db; +use crate::public_key::PublicKey; // the files here are just copied from iroh-smol-kv-uniffi/src/code mod kv { @@ -193,6 +192,16 @@ pub enum WriteError { PrivateKeySize { size: u64 }, } +/// Error shutting down the database. +/// +/// This can occur if the db is already shut down or if there is an internal error. +#[derive(uniffi::Enum, Snafu, Debug)] +#[snafu(module)] +pub enum ShutdownError { + /// Error during the shutdown operation. + Irpc { message: String }, +} + /// An entry returned from the database. #[derive(uniffi::Record, Debug, PartialEq, Eq)] pub struct Entry { @@ -468,6 +477,13 @@ impl Db { )), }) } + + /// Shutdown the database client and all subscriptions. + pub async fn shutdown(&self) -> Result<(), ShutdownError> { + Ok(self.0.shutdown().await.map_err(|e| ShutdownError::Irpc { + message: e.to_string(), + })?) + } } mod util { diff --git a/rust/iroh-streamplace/src/node/streams.rs b/rust/iroh-streamplace/src/node/streams.rs index abd696a7..95d2e7c5 100644 --- a/rust/iroh-streamplace/src/node/streams.rs +++ b/rust/iroh-streamplace/src/node/streams.rs @@ -10,7 +10,7 @@ use iroh_base::ticket::NodeTicket; use iroh_gossip::{net::Gossip, proto::TopicId}; use irpc::{WithChannels, rpc::RemoteService}; use irpc_iroh::{IrohProtocol, IrohRemoteConnection}; -use n0_future::future::Boxed; +use n0_future::{TryFutureExt, future::Boxed}; mod rpc { //! Protocol API @@ -105,6 +105,9 @@ mod api { #[derive(Debug, Serialize, Deserialize)] pub struct GetNodeAddr; + #[derive(Debug, Serialize, Deserialize)] + pub struct Shutdown; + // Use the macro to generate both the Protocol and Message enums // plus implement Channels for each type #[rpc_requests(message = Message)] @@ -122,15 +125,19 @@ mod api { AddTickets(AddTickets), #[rpc(tx=oneshot::Sender)] GetNodeAddr(GetNodeAddr), + #[rpc(tx=oneshot::Sender<()>)] + Shutdown(Shutdown), } } use api::{Message as ApiMessage, Protocol as ApiProtocol}; use n0_future::{FuturesUnordered, StreamExt}; use rpc::{Message as RpcMessage, Protocol as RpcProtocol}; use snafu::Snafu; +use tokio::sync::oneshot; use tracing::{Instrument, debug, error, trace, trace_span, warn}; use super::{Config, CreateError, JoinPeersError, PutError, db, streams::rpc::RecvSegment}; +use crate::node::ShutdownError; pub(crate) enum HandlerMode { Sender, @@ -312,7 +319,10 @@ impl Actor { let Some(msg) = msg else { break; }; - self.handle_api(msg).instrument(trace_span!("api")).await; + if let Some(shutdown) = self.handle_api(msg).instrument(trace_span!("api")).await { + shutdown.send(()).ok(); + break; + } } res = self.tasks.next(), if !self.tasks.is_empty() => { let Some((remote_id, res)) = res else { @@ -425,7 +435,7 @@ impl Actor { } } - async fn handle_api(&mut self, msg: ApiMessage) { + async fn handle_api(&mut self, msg: ApiMessage) -> Option> { match msg { ApiMessage::SendSegment(msg) => { trace!("{:?}", msg.inner); @@ -525,7 +535,11 @@ impl Actor { let addr = self.router.endpoint().node_addr().initialized().await; tx.send(addr).await.ok(); } + ApiMessage::Shutdown(msg) => { + return Some(msg.tx); + } } + None } fn handle_send( @@ -772,4 +786,24 @@ impl Node { })?; Ok(Arc::new(addr.node_id.into())) } + + /// Shutdown the node, including the streaming system and the metadata db. + pub async fn shutdown(&self) -> Result<(), ShutdownError> { + // shut down both the streams and the db concurrently, even if one fails + let (res1, res2) = tokio::join!(self.shutdown_streams(), self.client.shutdown()); + res1?; + res2?; + Ok(()) + } +} + +impl Node { + async fn shutdown_streams(&self) -> std::result::Result<(), ShutdownError> { + self.api + .rpc(api::Shutdown) + .await + .map_err(|e| ShutdownError::Irpc { + message: e.to_string(), + }) + } } diff --git a/rust/iroh-streamplace/src/node_addr.rs b/rust/iroh-streamplace/src/node_addr.rs index 6796b351..966c8f14 100644 --- a/rust/iroh-streamplace/src/node_addr.rs +++ b/rust/iroh-streamplace/src/node_addr.rs @@ -53,7 +53,6 @@ pub enum NodeAddrError { InvalidNetworkAddress { source: std::net::AddrParseError }, } - impl TryFrom for iroh::NodeAddr { type Error = NodeAddrError; -- 2.51.2 From 26e7b084dc44144141f647862abda480b57bb5f2 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 7 Oct 2025 17:30:55 +0300 Subject: [PATCH 129/525] Remove unused import from main.go test code --- pkg/iroh/iroh_test/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/iroh/iroh_test/main.go b/pkg/iroh/iroh_test/main.go index 7b8361f8..bb59684a 100644 --- a/pkg/iroh/iroh_test/main.go +++ b/pkg/iroh/iroh_test/main.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "fmt" "os" - "reflect" iroh "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" _ "stream.place/streamplace/pkg/streamplacedeps" -- 2.51.2 From 165f4cdaca5bf0b1a82503850a7349e07e15e5a4 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 7 Oct 2025 17:31:18 +0300 Subject: [PATCH 130/525] Change encoding from order preserving to just postcard (varint prefixed vec) --- rust/iroh-streamplace/src/node/db.rs | 73 +---------------------- rust/iroh-streamplace/src/node/streams.rs | 2 +- 2 files changed, 3 insertions(+), 72 deletions(-) diff --git a/rust/iroh-streamplace/src/node/db.rs b/rust/iroh-streamplace/src/node/db.rs index 944f3954..591c4e1d 100644 --- a/rust/iroh-streamplace/src/node/db.rs +++ b/rust/iroh-streamplace/src/node/db.rs @@ -492,7 +492,7 @@ mod util { let mut result = Vec::new(); if let Some(s) = stream { result.push(b's'); - escape_into([s], &mut result); + postcard::to_io(s, &mut result).unwrap(); } else { result.push(b'g'); } @@ -503,7 +503,7 @@ mod util { pub fn decode_stream_and_key(encoded: &[u8]) -> Option<(Option>, Vec)> { match encoded.split_first() { Some((b's', mut rest)) => { - let stream = unescape_one(&mut rest)?; + let (stream, rest) = postcard::take_from_bytes(&mut rest).ok()?; Some((Some(stream), rest.to_vec())) } Some((b'g', rest)) => Some((None, rest.to_vec())), @@ -511,75 +511,6 @@ mod util { } } - // these values are needed to keep the order preserved - const ESCAPE: u8 = 1; - const SEPARATOR: u8 = 0; - - /// Escape into an existing vec. - fn escape_into(components: I, result: &mut Vec) - where - I: IntoIterator, - C: AsRef<[u8]>, - { - for segment in components.into_iter() { - for &byte in segment.as_ref() { - match byte { - ESCAPE => result.extend([ESCAPE, ESCAPE]), - SEPARATOR => result.extend([ESCAPE, SEPARATOR]), - _ => result.push(byte), - } - } - result.push(SEPARATOR); - } - // you might think that the trailing separator is unnecessary, but it is needed - // to distinguish between the empty path and the path with one empty component - } - - fn unescape_one(path: &mut &[u8]) -> Option> { - let mut segment = Vec::new(); - let mut escape = false; - for (i, &byte) in path.iter().enumerate() { - if escape { - segment.push(byte); - escape = false; - } else { - match byte { - ESCAPE => escape = true, - SEPARATOR => { - *path = &path[i + 1..]; - return Some(segment); - } - _ => segment.push(byte), - } - } - } - None - } - - /// A simple version of unescape. - #[allow(dead_code)] - fn unescape(path: &[u8]) -> Vec> { - let mut components = Vec::new(); - let mut segment = Vec::new(); - let mut escape = false; - for &byte in path { - if escape { - segment.push(byte); - escape = false; - } else { - match byte { - ESCAPE => escape = true, - SEPARATOR => { - components.push(segment); - segment = Vec::new(); - } - _ => segment.push(byte), - } - } - } - components - } - pub fn format_bytes(bytes: &[u8]) -> String { if bytes.is_empty() { return "\"\"".to_string(); diff --git a/rust/iroh-streamplace/src/node/streams.rs b/rust/iroh-streamplace/src/node/streams.rs index 95d2e7c5..37be96d5 100644 --- a/rust/iroh-streamplace/src/node/streams.rs +++ b/rust/iroh-streamplace/src/node/streams.rs @@ -320,7 +320,7 @@ impl Actor { break; }; if let Some(shutdown) = self.handle_api(msg).instrument(trace_span!("api")).await { - shutdown.send(()).ok(); + shutdown.send(()).await.ok(); break; } } -- 2.51.2 From fc022926038f17718ff371b585a2c9a0c730f6f9 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Tue, 7 Oct 2025 17:43:26 +0300 Subject: [PATCH 131/525] Add shut down after 5s to the example. --- pkg/iroh/iroh_test/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/iroh/iroh_test/main.go b/pkg/iroh/iroh_test/main.go index bb59684a..cef2ee44 100644 --- a/pkg/iroh/iroh_test/main.go +++ b/pkg/iroh/iroh_test/main.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "fmt" "os" + "time" iroh "stream.place/streamplace/pkg/iroh/generated/iroh_streamplace" _ "stream.place/streamplace/pkg/streamplacedeps" @@ -69,10 +70,19 @@ func main() { panicIfErr(err) fmt.Printf("Iter items: %+v\n", items3) + go func() { + time.Sleep(5 * time.Second) + node.Shutdown() // or whatever your shutdown method is + }() + sub := db.Subscribe(iroh.NewFilter()) for { ev, err := sub.NextRaw() panicIfErr(err) + if ev == nil { + fmt.Println("Subscription closed") + break + } switch (*ev).(type) { case iroh.SubscribeItemEntry: fmt.Printf("%+v\n", (*ev).(iroh.SubscribeItemEntry)) -- 2.51.2 From c8c177a75499146f855de04432b0048dbbf386a2 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 7 Oct 2025 13:09:23 -0400 Subject: [PATCH 132/525] Add C2PA metadata features with user consent - Implement content metadata configuration (warnings, rights, distribution policy) - Add content rights UI with license selection and copyright info - Add content warnings UI with predefined and custom warnings - Add distribution policy with deleteAfter (duration in seconds) - Integrate manifest builder for C2PA signing - Add content filtering for warnings and distribution policies - Update segment model to include metadata from C2PA manifests - Add metadata form components for user consent workflow - Update lexicons for metadata schema - Fix deleteAfter type from string to number --- content-filters-example.json | 9 + .../live-dashboard/livestream-panel.tsx | 26 +- js/app/components/mobile/bottom-metadata.tsx | 19 + js/app/components/mobile/ui.tsx | 117 +- .../features/bluesky/contentMetadataSlice.tsx | 385 +++++++ js/app/store/store.tsx | 2 + .../content-metadata-form.tsx | 753 ++++++++++++ .../content-metadata/content-rights.tsx | 104 ++ .../content-metadata/content-warnings.tsx | 100 ++ .../src/components/content-metadata/index.tsx | 18 + js/components/src/components/ui/checkbox.tsx | 147 +++ js/components/src/components/ui/select.tsx | 175 +++ js/components/src/components/ui/tooltip.tsx | 131 +++ js/components/src/index.tsx | 3 + js/components/src/lib/metadata-constants.ts | 180 +++ .../content-metadata-actions.tsx | 142 +++ .../streamplace-store/streamplace-store.tsx | 34 +- .../place-stream-metadata-configuration.md | 62 + .../place-stream-metadata-contentrights.md | 198 ++++ .../place-stream-metadata-contentwarnings.md | 209 ++++ ...lace-stream-metadata-distributionpolicy.md | 45 + .../lex-reference/place-stream-segment.md | 35 +- .../place/stream/metadata/configuration.json | 28 + .../place/stream/metadata/contentRights.json | 74 ++ .../stream/metadata/contentWarnings.json | 70 ++ .../stream/metadata/distributionPolicy.json | 16 + lexicons/place/stream/segment.json | 12 + pkg/api/stream_key.go | 15 +- pkg/atproto/firehose.go | 1 + pkg/atproto/sync.go | 15 + pkg/cmd/sign.go | 22 +- pkg/cmd/streamplace.go | 7 +- pkg/config/config.go | 13 + pkg/constants/constants.go | 75 +- pkg/gen/gen.go | 4 + pkg/media/manifest_builder.go | 210 ++++ pkg/media/media.go | 174 ++- pkg/media/media_signer.go | 104 +- pkg/media/media_signer_ext.go | 52 +- pkg/media/segmenter.go | 2 + pkg/media/validate.go | 73 +- pkg/model/default_metadata.go | 57 + pkg/model/model.go | 5 + pkg/model/segment.go | 148 ++- pkg/streamplace/cbor_gen.go | 1012 ++++++++++++++++- pkg/streamplace/metadataconfiguration.go | 20 + pkg/streamplace/metadatacontentRights.go | 21 + pkg/streamplace/metadatacontentWarnings.go | 12 + pkg/streamplace/metadatadistributionPolicy.go | 13 + pkg/streamplace/streamsegment.go | 9 +- 50 files changed, 4984 insertions(+), 174 deletions(-) create mode 100644 content-filters-example.json create mode 100644 js/app/features/bluesky/contentMetadataSlice.tsx create mode 100644 js/components/src/components/content-metadata/content-metadata-form.tsx create mode 100644 js/components/src/components/content-metadata/content-rights.tsx create mode 100644 js/components/src/components/content-metadata/content-warnings.tsx create mode 100644 js/components/src/components/content-metadata/index.tsx create mode 100644 js/components/src/components/ui/checkbox.tsx create mode 100644 js/components/src/components/ui/select.tsx create mode 100644 js/components/src/components/ui/tooltip.tsx create mode 100644 js/components/src/lib/metadata-constants.ts create mode 100644 js/components/src/streamplace-store/content-metadata-actions.tsx create mode 100644 js/docs/src/content/docs/lex-reference/metadata/place-stream-metadata-configuration.md create mode 100644 js/docs/src/content/docs/lex-reference/metadata/place-stream-metadata-contentrights.md create mode 100644 js/docs/src/content/docs/lex-reference/metadata/place-stream-metadata-contentwarnings.md create mode 100644 js/docs/src/content/docs/lex-reference/metadata/place-stream-metadata-distributionpolicy.md create mode 100644 lexicons/place/stream/metadata/configuration.json create mode 100644 lexicons/place/stream/metadata/contentRights.json create mode 100644 lexicons/place/stream/metadata/contentWarnings.json create mode 100644 lexicons/place/stream/metadata/distributionPolicy.json create mode 100644 pkg/media/manifest_builder.go create mode 100644 pkg/model/default_metadata.go create mode 100644 pkg/streamplace/metadataconfiguration.go create mode 100644 pkg/streamplace/metadatacontentRights.go create mode 100644 pkg/streamplace/metadatacontentWarnings.go create mode 100644 pkg/streamplace/metadatadistributionPolicy.go diff --git a/content-filters-example.json b/content-filters-example.json new file mode 100644 index 00000000..bf756948 --- /dev/null +++ b/content-filters-example.json @@ -0,0 +1,9 @@ +{ + "content_warnings": { + "enabled": true, + "blocked_warnings": ["cwarn:violence", "cwarn:nudity", "cwarn:death"] + }, + "distribution_policy": { + "enabled": true + } +} diff --git a/js/app/components/live-dashboard/livestream-panel.tsx b/js/app/components/live-dashboard/livestream-panel.tsx index a19565f2..35bc5ec1 100644 --- a/js/app/components/live-dashboard/livestream-panel.tsx +++ b/js/app/components/live-dashboard/livestream-panel.tsx @@ -1,4 +1,7 @@ import { + Button, + ContentMetadataForm, + Textarea, useCreateStreamRecord, useLivestream, useToast, @@ -15,8 +18,6 @@ import { TouchableOpacity, View, } from "react-native"; -import { Button } from "../../../components/src/components/ui/button"; -import { Textarea } from "../../../components/src/components/ui/textarea"; import { selectUserProfile } from "../../features/bluesky/blueskySlice"; import { useCaptureVideoFrame } from "../../hooks/useCaptureVideoFrame"; import { useLiveUser } from "../../hooks/useLiveUser"; @@ -170,13 +171,16 @@ function LivestreamPanel() { const [selectedImage, setSelectedImage] = useState< string | File | Blob | undefined >(); - const [mode, setMode] = useState<"create" | "edit">( + const [mode, setMode] = useState<"create" | "edit" | "metadata">( livestream ? "edit" : "create", ); - const handleModeChange = useCallback((newMode: "create" | "edit") => { - setMode(newMode); - }, []); + const handleModeChange = useCallback( + (newMode: "create" | "edit" | "metadata") => { + setMode(newMode); + }, + [], + ); const handleSubmit = useCallback(async () => { if (!title.trim()) return; @@ -327,6 +331,7 @@ function LivestreamPanel() { values={[ { label: "Create", value: "create" }, { label: "Edit", value: "edit" }, + { label: "Metadata", value: "metadata" }, ]} style={[{ marginVertical: -2 }]} selectedValue={mode} @@ -349,7 +354,16 @@ function LivestreamPanel() { No active livestream to edit. Start a livestream first! + ) : mode === "metadata" ? ( + // Metadata view + + + ) : ( + // Create/Edit view x.livestream); + const segment = useLivestreamStore((x) => x.segment); const did = useDID(); + // Get content warnings and rights directly from the latest segment + const contentWarnings = + (segment?.contentWarnings?.warnings as string[]) || []; + const contentRights = segment?.contentRights; + return ( + + {/* Content Metadata - Below the main profile/controls bar */} + {(contentWarnings.length > 0 || + (contentRights && Object.keys(contentRights).length > 0)) && ( + + + {contentRights && ( + + )} + + )} ); } diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index f318e567..ce5f7685 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -1,5 +1,7 @@ import { useNavigation } from "@react-navigation/native"; import { + ContentRights, + ContentWarnings, PlayerUI, Slider, Text, @@ -7,6 +9,7 @@ import { useAvatars, useCameraToggle, useLivestreamInfo, + useLivestreamStore, useMuted, usePlayerDimensions, usePlayerStore, @@ -67,6 +70,12 @@ export function MobileUi({ const { isPlayerRatioGreater } = useSegmentDimensions(); const { doSetIngestCamera } = useCameraToggle(); const avatars = useAvatars(profile?.did ? [profile?.did] : []); + const segment = useLivestreamStore((x) => x.segment); + + // Get content warnings and rights directly from the latest segment + const contentWarnings = + (segment?.contentWarnings?.warnings as string[]) || []; + const contentRights = segment?.contentRights; const muteWasForced = usePlayerStore((state) => state.muteWasForced); const setMuteWasForced = usePlayerStore((state) => state.setMuteWasForced); @@ -166,51 +175,87 @@ export function MobileUi({ {/* Top Left - Back Button and Profile */} - - { - navigation.canGoBack() - ? navigation.goBack() - : navigation.navigate("Home", { screen: "StreamList" }); - }} + + - - - - {profile?.handle} + { + navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate("Home", { + screen: "StreamList", + }); + }} + > + + + + {profile?.handle} + + {/* Content Metadata - Below mute button */} + {(contentWarnings.length > 0 || + (contentRights && Object.keys(contentRights).length > 0)) && ( + + + {contentRights && ( + + )} + + )} + {/* Right Controls Column */} ({ + createContentMetadata: create.asyncThunk( + async ( + { + contentWarnings = [], + distributionPolicy = { + deleteAfter: undefined, + }, + contentRights = {}, + }: { + contentWarnings?: string[]; + distributionPolicy?: { + deleteAfter?: number; + }; + contentRights?: { + creator?: string; + copyrightNotice?: string; + copyrightYear?: number; + license?: string; + creditLine?: string; + }; + }, + thunkAPI, + ) => { + const { bluesky } = thunkAPI.getState() as { + bluesky: BlueskyState; + }; + + if (!bluesky.pdsAgent) { + throw new Error("No agent"); + } + + const did = bluesky.oauthSession?.did; + if (!did) { + throw new Error("No DID"); + } + + const metadataRecord = { + $type: "place.stream.metadata.configuration", + createdAt: new Date().toISOString(), + ...(contentWarnings.length > 0 && { + contentWarnings: { warnings: contentWarnings }, + }), + ...(distributionPolicy.deleteAfter && { distributionPolicy }), + ...(contentRights && + Object.keys(contentRights).length > 0 && { + contentRights, + }), + }; + + const result = await bluesky.pdsAgent.com.atproto.repo.createRecord({ + repo: did, + collection: "place.stream.metadata.configuration", + rkey: "self", + record: metadataRecord, + }); + + // Extract rkey from the URI + const rkey = result.data.uri.split("/").pop(); + + return { + record: metadataRecord, + uri: result.data.uri, + cid: result.data.cid, + rkey, + }; + }, + { + pending: (state) => { + return { + ...state, + creating: true, + error: null, + }; + }, + fulfilled: (state, action) => { + return { + ...state, + creating: false, + error: null, + lastCreatedRecord: action.payload, + }; + }, + rejected: (state, action) => { + return { + ...state, + creating: false, + error: action.error?.message ?? "Failed to create content metadata", + }; + }, + }, + ), + + updateContentMetadata: create.asyncThunk( + async ( + { + rkey, + livestreamRef, + contentWarnings = [], + distributionPolicy = { + deleteAfter: undefined, // No expiration means forever + }, + contentRights = {}, + }: { + rkey?: string; + livestreamRef?: { + uri: string; + cid: string; + }; + contentWarnings?: string[]; + distributionPolicy?: { + deleteAfter?: number; + }; + contentRights?: { + creator?: string; + copyrightNotice?: string; + copyrightYear?: number; + license?: string; + creditLine?: string; + }; + }, + thunkAPI, + ) => { + const { bluesky } = thunkAPI.getState() as { + bluesky: BlueskyState; + }; + + if (!bluesky.pdsAgent) { + throw new Error("No agent"); + } + + const did = bluesky.oauthSession?.did; + if (!did) { + throw new Error("No DID"); + } + + const metadataRecord = { + $type: "place.stream.metadata.configuration", + ...(livestreamRef && { livestreamRef }), + createdAt: new Date().toISOString(), + ...(contentWarnings.length > 0 && { + contentWarnings: { warnings: contentWarnings }, + }), + ...(distributionPolicy.deleteAfter && { distributionPolicy }), + ...(contentRights && + Object.keys(contentRights).length > 0 && { + contentRights, + }), + }; + + const result = await bluesky.pdsAgent.com.atproto.repo.putRecord({ + repo: did, + collection: "place.stream.metadata.configuration", + rkey: "self", + record: metadataRecord, + }); + + return { + record: metadataRecord, + uri: `at://${did}/place.stream.metadata.configuration/self`, + cid: result.data.cid, + }; + }, + { + pending: (state) => { + return { + ...state, + updating: true, + error: null, + }; + }, + fulfilled: (state, action) => { + return { + ...state, + updating: false, + error: null, + lastCreatedRecord: action.payload, + }; + }, + rejected: (state, action) => { + return { + ...state, + updating: false, + error: action.error?.message ?? "Failed to update content metadata", + }; + }, + }, + ), + + getContentMetadata: create.asyncThunk( + async ( + { userDid, rkey = "self" }: { userDid?: string; rkey?: string } = {}, + thunkAPI, + ) => { + const { bluesky } = thunkAPI.getState() as { + bluesky: BlueskyState; + }; + + if (!bluesky.pdsAgent) { + throw new Error("No agent"); + } + + // Use provided userDid or fall back to current user's DID + const targetDid = userDid || bluesky.oauthSession?.did; + if (!targetDid) { + throw new Error("No DID provided or user not authenticated"); + } + + // Add debugging information + console.log(`[getContentMetadata] Debug info:`, { + targetDid, + rkey, + pdsAgentType: bluesky.pdsAgent.constructor.name, + hasOAuthSession: !!bluesky.oauthSession, + currentUserDid: bluesky.oauthSession?.did, + pdsAgentHost: + (bluesky.pdsAgent as any)?.host || + (bluesky.pdsAgent as any)?.service?.host || + "unknown", + pdsAgentUrl: + (bluesky.pdsAgent as any)?.url || + (bluesky.pdsAgent as any)?.service?.url || + "unknown", + }); + + try { + // First, try to resolve the correct PDS for the target user + let targetPDS = null; + try { + const didResponse = await fetch( + `https://plc.directory/${targetDid}`, + ); + if (didResponse.ok) { + const didDoc = await didResponse.json(); + const pdsService = didDoc.service?.find( + (s: any) => s.id === "#atproto_pds", + ); + if (pdsService) { + targetPDS = pdsService.serviceEndpoint; + console.log( + `[getContentMetadata] Resolved PDS for ${targetDid}:`, + targetPDS, + ); + } + } + } catch (pdsResolveError) { + console.log( + `[getContentMetadata] Failed to resolve PDS for ${targetDid}:`, + pdsResolveError, + ); + } + + // Use the target PDS if available, otherwise fall back to the current agent + let agent = bluesky.pdsAgent; + if (targetPDS && targetPDS !== (bluesky.pdsAgent as any)?.host) { + // Create a new agent pointing to the target PDS + const { StreamplaceAgent } = await import("streamplace"); + agent = new StreamplaceAgent(targetPDS) as any; + console.log( + `[getContentMetadata] Created new agent for PDS:`, + targetPDS, + ); + } + + console.log(`[getContentMetadata] Attempting to fetch record from:`, { + repo: targetDid, + collection: "place.stream.metadata.configuration", + rkey, + usingPDS: targetPDS || "default", + }); + + const result = await agent.com.atproto.repo.getRecord({ + repo: targetDid, + collection: "place.stream.metadata.configuration", + rkey, + }); + + console.log(`[getContentMetadata] API response:`, result); + + if (!result.success) { + throw new Error("Failed to get content metadata record"); + } + + return { + userDid: targetDid, + record: result.data.value, + uri: result.data.uri, + cid: result.data.cid, + }; + } catch (error) { + console.log(`[getContentMetadata] Error details:`, { + error: error.message, + errorType: error.constructor.name, + errorStack: error.stack, + }); + + // If user doesn't have metadata record, return null instead of throwing + if ( + error.message?.includes("not found") || + error.message?.includes("RecordNotFound") + ) { + return { + userDid: targetDid, + record: null, + uri: null, + cid: null, + }; + } + throw error; + } + }, + { + pending: (state) => { + return { + ...state, + error: null, + }; + }, + fulfilled: (state, action) => { + return { + ...state, + error: null, + lastCreatedRecord: action.payload, + }; + }, + rejected: (state, action) => { + return { + ...state, + error: action.error?.message ?? "Failed to get content metadata", + }; + }, + }, + ), + + clearError: create.reducer((state) => { + return { + ...state, + error: null, + }; + }), + }), + + selectors: { + selectContentMetadata: (state) => state, + selectIsCreating: (state) => state.creating, + selectIsUpdating: (state) => state.updating, + selectError: (state) => state.error, + selectLastCreatedRecord: (state) => state.lastCreatedRecord, + }, +}); + +export const { + createContentMetadata, + updateContentMetadata, + getContentMetadata, + clearError, +} = contentMetadataSlice.actions; + +export const { + selectContentMetadata, + selectIsCreating, + selectIsUpdating, + selectError, + selectLastCreatedRecord, +} = contentMetadataSlice.selectors; diff --git a/js/app/store/store.tsx b/js/app/store/store.tsx index 1d06cfc4..293c156b 100644 --- a/js/app/store/store.tsx +++ b/js/app/store/store.tsx @@ -4,6 +4,7 @@ import { setupListeners } from "@reduxjs/toolkit/query"; import { baseSlice } from "features/base/baseSlice"; import { sidebarSlice } from "features/base/sidebarSlice"; import { blueskySlice } from "features/bluesky/blueskySlice"; +import { contentMetadataSlice } from "features/bluesky/contentMetadataSlice"; import { platformSlice } from "features/platform/platformSlice"; import { streamplaceSlice } from "features/streamplace/streamplaceSlice"; @@ -11,6 +12,7 @@ import { listenerMiddleware } from "./listener"; const rootReducer = combineSlices( blueskySlice, + contentMetadataSlice, streamplaceSlice, platformSlice, sidebarSlice, diff --git a/js/components/src/components/content-metadata/content-metadata-form.tsx b/js/components/src/components/content-metadata/content-metadata-form.tsx new file mode 100644 index 00000000..41061e48 --- /dev/null +++ b/js/components/src/components/content-metadata/content-metadata-form.tsx @@ -0,0 +1,753 @@ +import { forwardRef, useCallback, useEffect, useState } from "react"; +import { ScrollView, View } from "react-native"; +import { + CONTENT_WARNINGS, + LICENSE_OPTIONS, +} from "../../lib/metadata-constants"; + +import { + useGetContentMetadata, + useSaveContentMetadata, +} from "../../streamplace-store/content-metadata-actions"; +import { useDID } from "../../streamplace-store/streamplace-store"; +import { usePDSAgent } from "../../streamplace-store/xrpc"; +import * as zero from "../../ui"; +import { Button } from "../ui/button"; +import { Checkbox } from "../ui/checkbox"; +import { Input } from "../ui/input"; +import { Select } from "../ui/select"; +import { Text } from "../ui/text"; +import { Textarea } from "../ui/textarea"; +import { toast, useToast } from "../ui/toast"; +import { Tooltip } from "../ui/tooltip"; + +const { p, r, bg, borders, w, text, layout, gap, flex } = zero; + +// Types +export interface DistributionPolicy { + deleteAfter?: number; +} + +export interface Rights { + creator?: string; + copyrightNotice?: string; + copyrightYear?: string | number; + license?: string; + creditLine?: string; +} + +export interface ContentMetadata { + contentWarnings: { warnings: string[] }; + distributionPolicy: DistributionPolicy; + contentRights: Rights; +} + +export interface ContentMetadataFormProps { + showUpdateButton?: boolean; + onMetadataChange?: (metadata: ContentMetadata) => void; + initialMetadata?: ContentMetadata; + style?: any; +} + +// ButtonSelector component (same as in livestream-panel) +const ButtonSelector = ({ + values, + selectedValue, + setSelectedValue, + disabledValues = [], + style = [], +}: { + values: { label: string; value: string }[]; + selectedValue: string; + setSelectedValue: (value: string) => void; + disabledValues?: string[]; + style?: any[]; +}) => ( + + {values.map(({ label, value }) => ( + + ))} + +); + +export const ContentMetadataForm = forwardRef( + ( + { showUpdateButton = false, onMetadataChange, initialMetadata, style }, + ref, + ) => { + const pdsAgent = usePDSAgent(); + const did = useDID(); + const getContentMetadata = useGetContentMetadata(); + const saveContentMetadata = useSaveContentMetadata(); + const { toast: toastState } = useToast(); + + // Local state for metadata + const [contentWarnings, setContentWarnings] = useState([]); + const [distributionPolicy, setDistributionPolicy] = + useState({}); + const [contentRights, setContentRights] = useState({}); + const [selectedLicense, setSelectedLicense] = useState(""); + const [customLicenseText, setCustomLicenseText] = useState(""); + const [customDateTime, setCustomDateTime] = useState(""); + const [loading, setLoading] = useState(false); + const [hasMetadata, setHasMetadata] = useState(false); + + // State for section toggles + const [activeSection, setActiveSection] = + useState("contentWarnings"); + + const currentYear = new Date().getFullYear(); + + // Load existing metadata on mount or from initialMetadata prop + useEffect(() => { + if (initialMetadata) { + // Use provided initial metadata + if (initialMetadata.contentWarnings?.warnings) { + setContentWarnings(initialMetadata.contentWarnings.warnings); + } + if (initialMetadata.distributionPolicy) { + setDistributionPolicy(initialMetadata.distributionPolicy); + setCustomDateTime( + initialMetadata.distributionPolicy.deleteAfter + ? String(initialMetadata.distributionPolicy.deleteAfter) + : "", + ); + } + if (initialMetadata.contentRights) { + setContentRights(initialMetadata.contentRights); + setSelectedLicense(initialMetadata.contentRights.license || ""); + } + return; + } + + const loadMetadata = async () => { + if (!pdsAgent || !did) return; + + try { + const metadata = await getContentMetadata(); + if (metadata?.record) { + setHasMetadata(true); + if (metadata.record.contentWarnings?.warnings) { + setContentWarnings( + metadata.record.contentWarnings.warnings as string[], + ); + } + if (metadata.record.distributionPolicy) { + setDistributionPolicy(metadata.record.distributionPolicy); + setCustomDateTime( + metadata.record.distributionPolicy.deleteAfter + ? String(metadata.record.distributionPolicy.deleteAfter) + : "", + ); + } + if (metadata.record.contentRights) { + setContentRights(metadata.record.contentRights); + setSelectedLicense(metadata.record.contentRights.license || ""); + } + } + } catch (error) { + // No existing metadata is fine + console.log("No existing metadata found"); + } + }; + + loadMetadata(); + }, [pdsAgent, did, initialMetadata]); + + const handleContentWarningChange = useCallback( + (warning: string, checked: boolean) => { + const newWarnings = checked + ? [...contentWarnings, warning] + : contentWarnings.filter((w) => w !== warning); + + setContentWarnings(newWarnings); + + if (onMetadataChange) { + onMetadataChange({ + contentWarnings: { warnings: newWarnings }, + distributionPolicy, + contentRights, + }); + } + }, + [contentWarnings, distributionPolicy, contentRights, onMetadataChange], + ); + + // Notify parent component when metadata changes + useEffect(() => { + if (onMetadataChange) { + onMetadataChange({ + contentWarnings: { warnings: contentWarnings }, + distributionPolicy, + contentRights, + }); + } + }, [contentWarnings, distributionPolicy, contentRights, onMetadataChange]); + + // Handle distribution policy changes + const handleDistributionPolicyChange = useCallback( + (deleteAfter: string) => { + const duration = parseInt(deleteAfter, 10); + const newPolicy = + deleteAfter.trim() !== "" && !isNaN(duration) + ? { deleteAfter: duration } + : {}; + setDistributionPolicy(newPolicy); + + if (onMetadataChange) { + onMetadataChange({ + contentWarnings: { warnings: contentWarnings }, + distributionPolicy: newPolicy, + contentRights, + }); + } + }, + [contentWarnings, contentRights, onMetadataChange], + ); + + // Handle content rights changes + const handleContentRightsChange = useCallback( + (field: string, value: any) => { + const newRights = { ...contentRights, [field]: value }; + setContentRights(newRights); + + if (onMetadataChange) { + onMetadataChange({ + contentWarnings: { warnings: contentWarnings }, + distributionPolicy, + contentRights: newRights, + }); + } + }, + [contentWarnings, distributionPolicy, contentRights, onMetadataChange], + ); + + const handleSave = useCallback(async () => { + setLoading(true); + try { + // Build the metadata object, only including non-empty fields + const metadata: any = {}; + + // Only include contentWarnings if it has values + if (contentWarnings && contentWarnings.length > 0) { + metadata.contentWarnings = contentWarnings; + } + + // Only include distributionPolicy if it has a deleteAfter value + if (customDateTime && customDateTime.trim() !== "") { + const duration = parseInt(customDateTime, 10); + if (!isNaN(duration)) { + metadata.distributionPolicy = { deleteAfter: duration }; + } + } + + // Only include contentRights if it has actual values + const rightsWithLicense = { + ...contentRights, + license: + selectedLicense === "custom" + ? customLicenseText + : selectedLicense || undefined, + }; + + // Filter out empty values from contentRights and convert copyrightYear to number + const filteredRights = Object.fromEntries( + Object.entries(rightsWithLicense) + .filter( + ([_, value]) => + value !== undefined && value !== null && value !== "", + ) + .map(([key, value]) => { + // Convert copyrightYear to integer as per lexicon + if (key === "copyrightYear" && typeof value === "string") { + const year = parseInt(value, 10); + return [key, isNaN(year) ? undefined : year]; + } + return [key, value]; + }) + .filter(([_, value]) => value !== undefined), + ); + + if (Object.keys(filteredRights).length > 0) { + metadata.contentRights = filteredRights; + } + + await saveContentMetadata(metadata); + setHasMetadata(true); + // Show success toast + toast.show( + hasMetadata ? "Content metadata updated" : "Content metadata created", + "Your settings have been saved successfully", + ); + } catch (error) { + console.error("Failed to save metadata:", error); + // Show error toast + toast.show("Failed to save metadata", "Please try again later"); + } finally { + setLoading(false); + } + }, [ + contentWarnings, + contentRights, + selectedLicense, + customLicenseText, + customDateTime, + hasMetadata, + saveContentMetadata, + ]); + + return ( + <> + + + {/* Section Selector */} + + + + + {/* Content Warnings Section */} + {activeSection === "contentWarnings" && ( + + + + Content Warnings + + + optional + + + + {CONTENT_WARNINGS.map((warning) => ( + + + + handleContentWarningChange(warning.value, checked) + } + label={warning.label} + style={[{ fontSize: 12 }]} + /> + + + ))} + + + )} + + {/* Content Rights Section */} + {activeSection === "contentRights" && ( + + + + Content Rights + + + optional + + + + + + + Copyright Year + + + + handleContentRightsChange("copyrightYear", value) + } + placeholder={currentYear.toString()} + variant="filled" + inputStyle={[ + p[3], + r.md, + bg.neutral[800], + text.white, + borders.width.thin, + borders.color.neutral[600], + w.percent[100], + ]} + /> + + + + + + License + + +