From f39d34b7696760f14cc46f89d144c6bad8191cd1 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 11:49:47 +0200 Subject: [PATCH 001/110] feat: :sparkles: Initialize rewrite --- .eslintrc.json | 3 + .github/workflows/deploy.yml | 61 --- .gitignore | 38 +- .vitepress/config.mts | 165 ------- .vitepress/theme/Layout.vue | 13 - .vitepress/theme/custom.css | 62 --- .vitepress/theme/index.ts | 8 - Dockerfile | 33 -- LICENSE | 21 - LICENSE.md | 129 ++++++ README.md | 46 +- app/attachments/page.mdx | 363 +++++++++++++++ app/authentication/page.mdx | 41 ++ app/contacts/page.mdx | 394 +++++++++++++++++ app/conversations/page.mdx | 407 +++++++++++++++++ app/errors/page.mdx | 70 +++ app/favicon.ico | Bin 0 -> 15086 bytes app/groups/page.mdx | 448 +++++++++++++++++++ app/layout.tsx | 43 ++ app/messages/page.mdx | 441 +++++++++++++++++++ app/not-found.tsx | 24 + app/page.mdx | 43 ++ app/pagination/page.mdx | 63 +++ app/providers.tsx | 37 ++ app/quickstart/page.mdx | 98 +++++ app/sdks/page.mdx | 17 + app/webhooks/page.mdx | 172 ++++++++ biome.json | 87 +++- bun.lockb | Bin 63252 -> 271604 bytes components/Banner.vue | 19 - components/Button.tsx | 82 ++++ components/Code.tsx | 393 +++++++++++++++++ components/Features.vue | 77 ---- components/Feedback.tsx | 110 +++++ components/Footer.tsx | 153 +++++++ components/GridPattern.tsx | 61 +++ components/Guides.tsx | 58 +++ components/Header.tsx | 104 +++++ components/Heading.tsx | 126 ++++++ components/HeroPattern.tsx | 32 ++ components/Layout.tsx | 47 ++ components/Libraries.tsx | 89 ++++ components/Logo.tsx | 16 + components/MobileNavigation.tsx | 182 ++++++++ components/Navigation.tsx | 292 +++++++++++++ components/Prose.tsx | 25 ++ components/Resources.tsx | 195 +++++++++ components/Search.tsx | 511 ++++++++++++++++++++++ components/SectionProvider.tsx | 165 +++++++ components/Tag.tsx | 58 +++ components/Team.vue | 112 ----- components/ThemeToggle.tsx | 46 ++ components/icons/BellIcon.tsx | 19 + components/icons/BoltIcon.tsx | 13 + components/icons/BookIcon.tsx | 19 + components/icons/CalendarIcon.tsx | 25 ++ components/icons/CartIcon.tsx | 17 + components/icons/ChatBubbleIcon.tsx | 19 + components/icons/CheckIcon.tsx | 19 + components/icons/ChevronRightLeftIcon.tsx | 19 + components/icons/ClipboardIcon.tsx | 19 + components/icons/CogIcon.tsx | 21 + components/icons/CopyIcon.tsx | 19 + components/icons/DocumentIcon.tsx | 19 + components/icons/EnvelopeIcon.tsx | 19 + components/icons/FaceSmileIcon.tsx | 19 + components/icons/FolderIcon.tsx | 24 + components/icons/LinkIcon.tsx | 14 + components/icons/ListIcon.tsx | 19 + components/icons/MagnifyingGlassIcon.tsx | 15 + components/icons/MapPinIcon.tsx | 21 + components/icons/PackageIcon.tsx | 18 + components/icons/PaperAirplaneIcon.tsx | 19 + components/icons/PaperClipIcon.tsx | 14 + components/icons/ShapesIcon.tsx | 19 + components/icons/ShirtIcon.tsx | 13 + components/icons/SquaresPlusIcon.tsx | 19 + components/icons/TagIcon.tsx | 21 + components/icons/UserIcon.tsx | 26 ++ components/icons/UsersIcon.tsx | 30 ++ components/mdx.tsx | 126 ++++++ docs/extensions.md | 95 ---- docs/extensions/custom-emojis.md | 57 --- docs/extensions/events.md | 5 - docs/extensions/interactivity.md | 10 - docs/extensions/is-cat.md | 27 -- docs/extensions/microblogging.md | 66 --- docs/extensions/migration.md | 47 -- docs/extensions/polls.md | 249 ----------- docs/extensions/reactions.md | 151 ------- docs/extensions/reports.md | 70 --- docs/extensions/server-endorsement.md | 78 ---- docs/extensions/vanity.md | 184 -------- docs/federation/endpoints.md | 198 --------- docs/federation/server-actor.md | 11 - docs/federation/user-discovery.md | 78 ---- docs/groups.md | 67 --- docs/index.md | 28 -- docs/objects.md | 62 --- docs/objects/actions.md | 52 --- docs/objects/actors.md | 48 -- docs/objects/announce.md | 1 - docs/objects/dislike.md | 42 -- docs/objects/follow-accept.md | 42 -- docs/objects/follow-reject.md | 42 -- docs/objects/follow.md | 42 -- docs/objects/like.md | 42 -- docs/objects/note.md | 13 - docs/objects/patch.md | 65 --- docs/objects/publications.md | 346 --------------- docs/objects/server-metadata.md | 174 -------- docs/objects/undo.md | 53 --- docs/objects/user.md | 355 --------------- docs/public/assets/boosting.png | Bin 43568 -> 0 bytes docs/public/assets/discord-buttons.webp | Bin 31498 -> 0 bytes docs/public/favicon.png | Bin 5613 -> 0 bytes docs/security/api.md | 116 ----- docs/security/keys.md | 51 --- docs/security/signing.md | 184 -------- docs/spec.md | 102 ----- docs/structures/collection.md | 33 -- docs/structures/content-format.md | 100 ----- docs/structures/custom-emoji.md | 42 -- images/logos/go.svg | 14 + images/logos/node.svg | 4 + images/logos/php.svg | 10 + images/logos/python.svg | 13 + images/logos/ruby.svg | 4 + lib/remToPx.ts | 10 + mdx-components.tsx | 10 + mdx/recma.mjs | 3 + mdx/rehype.mjs | 129 ++++++ mdx/remark.mjs | 4 + mdx/search.mjs | 141 ++++++ next.config.mjs | 21 + package.json | 74 +++- postcss.config.js | 6 + prettier.config.js | 6 + styles/tailwind.css | 21 + tailwind.config.ts | 53 +++ tsconfig.json | 28 ++ types.d.ts | 11 + typography.ts | 355 +++++++++++++++ 143 files changed, 7257 insertions(+), 4032 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .vitepress/config.mts delete mode 100644 .vitepress/theme/Layout.vue delete mode 100644 .vitepress/theme/custom.css delete mode 100644 .vitepress/theme/index.ts delete mode 100644 Dockerfile delete mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 app/attachments/page.mdx create mode 100644 app/authentication/page.mdx create mode 100644 app/contacts/page.mdx create mode 100644 app/conversations/page.mdx create mode 100644 app/errors/page.mdx create mode 100644 app/favicon.ico create mode 100644 app/groups/page.mdx create mode 100644 app/layout.tsx create mode 100644 app/messages/page.mdx create mode 100644 app/not-found.tsx create mode 100644 app/page.mdx create mode 100644 app/pagination/page.mdx create mode 100644 app/providers.tsx create mode 100644 app/quickstart/page.mdx create mode 100644 app/sdks/page.mdx create mode 100644 app/webhooks/page.mdx delete mode 100644 components/Banner.vue create mode 100644 components/Button.tsx create mode 100644 components/Code.tsx delete mode 100644 components/Features.vue create mode 100644 components/Feedback.tsx create mode 100644 components/Footer.tsx create mode 100644 components/GridPattern.tsx create mode 100644 components/Guides.tsx create mode 100644 components/Header.tsx create mode 100644 components/Heading.tsx create mode 100644 components/HeroPattern.tsx create mode 100644 components/Layout.tsx create mode 100644 components/Libraries.tsx create mode 100644 components/Logo.tsx create mode 100644 components/MobileNavigation.tsx create mode 100644 components/Navigation.tsx create mode 100644 components/Prose.tsx create mode 100644 components/Resources.tsx create mode 100644 components/Search.tsx create mode 100644 components/SectionProvider.tsx create mode 100644 components/Tag.tsx delete mode 100644 components/Team.vue create mode 100644 components/ThemeToggle.tsx create mode 100644 components/icons/BellIcon.tsx create mode 100644 components/icons/BoltIcon.tsx create mode 100644 components/icons/BookIcon.tsx create mode 100644 components/icons/CalendarIcon.tsx create mode 100644 components/icons/CartIcon.tsx create mode 100644 components/icons/ChatBubbleIcon.tsx create mode 100644 components/icons/CheckIcon.tsx create mode 100644 components/icons/ChevronRightLeftIcon.tsx create mode 100644 components/icons/ClipboardIcon.tsx create mode 100644 components/icons/CogIcon.tsx create mode 100644 components/icons/CopyIcon.tsx create mode 100644 components/icons/DocumentIcon.tsx create mode 100644 components/icons/EnvelopeIcon.tsx create mode 100644 components/icons/FaceSmileIcon.tsx create mode 100644 components/icons/FolderIcon.tsx create mode 100644 components/icons/LinkIcon.tsx create mode 100644 components/icons/ListIcon.tsx create mode 100644 components/icons/MagnifyingGlassIcon.tsx create mode 100644 components/icons/MapPinIcon.tsx create mode 100644 components/icons/PackageIcon.tsx create mode 100644 components/icons/PaperAirplaneIcon.tsx create mode 100644 components/icons/PaperClipIcon.tsx create mode 100644 components/icons/ShapesIcon.tsx create mode 100644 components/icons/ShirtIcon.tsx create mode 100644 components/icons/SquaresPlusIcon.tsx create mode 100644 components/icons/TagIcon.tsx create mode 100644 components/icons/UserIcon.tsx create mode 100644 components/icons/UsersIcon.tsx create mode 100644 components/mdx.tsx delete mode 100644 docs/extensions.md delete mode 100644 docs/extensions/custom-emojis.md delete mode 100644 docs/extensions/events.md delete mode 100644 docs/extensions/interactivity.md delete mode 100644 docs/extensions/is-cat.md delete mode 100644 docs/extensions/microblogging.md delete mode 100644 docs/extensions/migration.md delete mode 100644 docs/extensions/polls.md delete mode 100644 docs/extensions/reactions.md delete mode 100644 docs/extensions/reports.md delete mode 100644 docs/extensions/server-endorsement.md delete mode 100644 docs/extensions/vanity.md delete mode 100644 docs/federation/endpoints.md delete mode 100644 docs/federation/server-actor.md delete mode 100644 docs/federation/user-discovery.md delete mode 100644 docs/groups.md delete mode 100644 docs/index.md delete mode 100644 docs/objects.md delete mode 100644 docs/objects/actions.md delete mode 100644 docs/objects/actors.md delete mode 100644 docs/objects/announce.md delete mode 100644 docs/objects/dislike.md delete mode 100644 docs/objects/follow-accept.md delete mode 100644 docs/objects/follow-reject.md delete mode 100644 docs/objects/follow.md delete mode 100644 docs/objects/like.md delete mode 100644 docs/objects/note.md delete mode 100644 docs/objects/patch.md delete mode 100644 docs/objects/publications.md delete mode 100644 docs/objects/server-metadata.md delete mode 100644 docs/objects/undo.md delete mode 100644 docs/objects/user.md delete mode 100644 docs/public/assets/boosting.png delete mode 100644 docs/public/assets/discord-buttons.webp delete mode 100644 docs/public/favicon.png delete mode 100644 docs/security/api.md delete mode 100644 docs/security/keys.md delete mode 100644 docs/security/signing.md delete mode 100644 docs/spec.md delete mode 100644 docs/structures/collection.md delete mode 100644 docs/structures/content-format.md delete mode 100644 docs/structures/custom-emoji.md create mode 100644 images/logos/go.svg create mode 100644 images/logos/node.svg create mode 100644 images/logos/php.svg create mode 100644 images/logos/python.svg create mode 100644 images/logos/ruby.svg create mode 100644 lib/remToPx.ts create mode 100644 mdx-components.tsx create mode 100644 mdx/recma.mjs create mode 100644 mdx/rehype.mjs create mode 100644 mdx/remark.mjs create mode 100644 mdx/search.mjs create mode 100644 next.config.mjs create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 styles/tailwind.css create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types.d.ts create mode 100644 typography.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1c2aa65 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 7cfb52e..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Sample workflow for building and deploying a VitePress site to GitHub Pages -# -name: Deploy VitePress site to Pages - -on: - # Runs on pushes targeting the `main` branch. Change this to `master` if you're - # using the `master` branch as the default branch. - push: - branches: [main] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: pages - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Not needed if lastUpdated is not enabled - # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm - - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Install dependencies - run: bun install - - name: Build with VitePress - run: | - bun run docs:build - touch .vitepress/dist/.nojekyll - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: .vitepress/dist - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - needs: build - runs-on: ubuntu-latest - name: Deploy - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 2fa2eb0..8f322f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,35 @@ -node_modules -.vitepress/cache -.vitepress/dist \ No newline at end of file +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vitepress/config.mts b/.vitepress/config.mts deleted file mode 100644 index a790ff3..0000000 --- a/.vitepress/config.mts +++ /dev/null @@ -1,165 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vitepress"; - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "Lysand Documentation", - description: "Documentation for Lysand, a new federated protocol", - vite: { - plugins: [tailwindcss()], - }, - vue: { - template: { - compilerOptions: { - isCustomElement: (tag) => tag === "iconify-icon", - }, - }, - }, - srcDir: "docs", - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: "Home", link: "/" }, - { text: "Specification", link: "/spec" }, - { text: "Objects", link: "/objects" }, - { text: "Security", link: "/security/api" }, - { text: "Extensions", link: "/extensions" }, - ], - - sidebar: [ - { - text: "Specification", - items: [ - { text: "Spec", link: "/spec" }, - { text: "Objects", link: "/objects" }, - ], - }, - { - text: "Structures", - items: [ - { - text: "Content Format", - link: "/structures/content-format", - }, - { text: "Custom Emoji", link: "/structures/custom-emoji" }, - { text: "Collection", link: "/structures/collection" }, - ], - }, - { - text: "Groups", - items: [{ text: "Groups", link: "/groups" }], - }, - { - text: "Security", - items: [ - { text: "API", link: "/security/api" }, - { text: "Keys", link: "/security/keys" }, - { text: "Signing", link: "/security/signing" }, - ], - }, - { - text: "Objects", - items: [ - { - text: "Publications", - link: "/objects/publications", - items: [ - { text: "Note", link: "/objects/note" }, - { text: "Patch", link: "/objects/patch" }, - ], - }, - { - text: "Actors", - link: "/objects/actors", - items: [{ text: "User", link: "/objects/user" }], - }, - { - text: "Actions", - link: "/objects/actions", - items: [ - { text: "Like", link: "/objects/like" }, - { text: "Dislike", link: "/objects/dislike" }, - { text: "Follow", link: "/objects/follow" }, - { - text: "FollowAccept", - link: "/objects/follow-accept", - }, - { - text: "FollowReject", - link: "/objects/follow-reject", - }, - { text: "Announce", link: "/objects/announce" }, - { text: "Undo", link: "/objects/undo" }, - ], - }, - { - text: "Server Metadata", - link: "/objects/server-metadata", - }, - ], - }, - { - text: "Federation", - items: [ - { text: "Endpoints", link: "/federation/endpoints" }, - { - text: "User Discovery", - link: "/federation/user-discovery", - }, - { text: "Server Actors", link: "/federation/server-actor" }, - ], - }, - { - text: "Extensions", - link: "/extensions", - items: [ - { - text: "Custom Emojis", - link: "/extensions/custom-emojis", - }, - { - text: "Microblogging", - link: "/extensions/microblogging", - }, - { text: "Reactions", link: "/extensions/reactions" }, - { text: "Polls", link: "/extensions/polls" }, - { text: "Is Cat", link: "/extensions/is-cat" }, - { - text: "Server Endorsements", - link: "/extensions/server-endorsement", - }, - { text: "Events", link: "/extensions/events" }, - { text: "Reports", link: "/extensions/reports" }, - { text: "Migration", link: "/extensions/migration" }, - { text: "Vanity", link: "/extensions/vanity" }, - { - text: "Interactivity", - link: "/extensions/interactivity", - }, - ], - }, - ], - - footer: { - message: "Released under the MIT License.", - copyright: "Copyright © 2023-present Gaspard Wierzbinski", - }, - - socialLinks: [ - { icon: "github", link: "https://github.com/lysand-org/" }, - ], - search: { - provider: "local", - }, - editLink: { - pattern: "https://github.com/lysand-org/docs/edit/main/docs/:path", - }, - externalLinkIcon: true, - logo: "https://cdn.lysand.org/logo.webp", - }, - lastUpdated: true, - cleanUrls: true, - titleTemplate: ":title · Lysand Docs", - head: [["link", { rel: "icon", href: "/favicon.png", type: "image/png" }]], - lang: "en-US", -}); diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue deleted file mode 100644 index c070240..0000000 --- a/.vitepress/theme/Layout.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - \ No newline at end of file diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css deleted file mode 100644 index 7f7229d..0000000 --- a/.vitepress/theme/custom.css +++ /dev/null @@ -1,62 +0,0 @@ -@import "tailwindcss"; - -@theme { -} -:root { - /* --vp-home-hero-image-background-image: linear-gradient( - to top right, - rgb(249, 168, 212), - rgb(216, 180, 254), - rgb(129, 140, 248) - ); - --vp-home-hero-image-filter: brightness(0.8) saturate(1.2); */ - --vp-home-hero-name-color: rgb(249, 168, 212); - --vp-c-brand-1: rgb(249, 168, 212); - --vp-layout-top-height: var(--spacing-10); - --lysand-gradient: linear-gradient( - to right, - rgb(249, 168, 212), - rgb(216, 180, 254), - rgb(129, 140, 248) - ); - --vp-color-primary: rgb(249, 168, 212); - --vp-color-secondary: rgb(216, 180, 254); - --vp-button-brand-bg: transparent; - --vp-c-bg-soft: rgb(250, 250, 250); -} - -.dark { - --vp-c-bg: rgb(24, 24, 24); - --vp-c-bg-soft: rgb(32, 32, 32); -} - -.VPFeature { - border-radius: 0.3rem !important; - transition: all 0.2s ease-in-out !important; -} - -.VPFeature:hover { - transform: scale(1.02); - border-color: var(--vp-color-primary); -} - -.VPButton.medium { - border-radius: 0.3rem !important; - transition: all 0.2s ease-in-out !important; -} - -.VPButton.medium:hover { - transform: scale(1.02); -} - -.VPButton.brand { - background: var(--lysand-gradient); - border: none !important; -} - -@media (min-width: 960px) { - .image-container { - width: 50% !important; - margin-right: 0.5rem !important; - } -} diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts deleted file mode 100644 index f9c0381..0000000 --- a/.vitepress/theme/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import DefaultTheme from "vitepress/theme"; -import Layout from "./Layout.vue"; -import "./custom.css"; - -export default { - extends: DefaultTheme, - Layout, -}; diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 26e3a4c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM oven/bun:alpine as base - -# Install dependencies into temp directory -# This will cache them and speed up future builds -FROM base AS install -RUN mkdir -p /temp/dev -COPY package.json bun.lockb /temp/dev/ -RUN cd /temp/dev && bun install --frozen-lockfile - -# Install with --production (exclude devDependencies) -RUN mkdir -p /temp/prod -COPY package.json bun.lockb /temp/prod/ -RUN cd /temp/prod && bun install --frozen-lockfile --production - -FROM base AS builder - -COPY . /app -RUN cd /app && bun install -RUN cd /app && bun docs:build - -FROM base AS final - -COPY --from=builder /app/.vitepress/dist/ /app - -LABEL org.opencontainers.image.authors "Gaspard Wierzbinski (https://cpluspatch.com)" -LABEL org.opencontainers.image.source "https://github.com/lysand-org/docs" -LABEL org.opencontainers.image.vendor "Lysand.org" -LABEL org.opencontainers.image.licenses "MIT" -LABEL org.opencontainers.image.title "Lysand Docs" -LABEL org.opencontainers.image.description "Documentation for Lysand" - -WORKDIR /app -CMD ["bun", "docs:serve"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 41dfb3f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Gaspard Wierzbinski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3a28c7d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,129 @@ +# Tailwind UI License + +## Personal License + +Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. + +The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates. + +You **can**: + +- Use the Components and Templates to create unlimited End Products. +- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. +- Use the Components and Templates to create unlimited End Products for unlimited Clients. +- Use the Components and Templates to create End Products where the End Product is sold to End Users. +- Use the Components and Templates to create End Products that are open source and freely available to End Users. + +You **cannot**: + +- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates. +- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets. +- Share your access to the Components and Templates with any other individuals. +- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. + +### Example usage + +Examples of usage **allowed** by the license: + +- Creating a personal website by yourself. +- Creating a website or web application for a client that will be owned by that client. +- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. +- Creating a commercial self-hosted web application that is sold to end users for a one-time fee. +- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available. + +Examples of usage **not allowed** by the license: + +- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly. +- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free. +- Create a Figma or Sketch UI kit based on the Tailwind UI component designs. +- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI. +- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free. +- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. + +In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI. + +### Personal License Definitions + +Licensee is the individual who has purchased a Personal License. + +Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license. + +End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. + +End User is a user of an End Product. + +Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. + +## Team License + +Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates. + +The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates. + +You **can**: + +- Use the Components and Templates to create unlimited End Products. +- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license. +- Use the Components and Templates to create unlimited End Products for unlimited Clients. +- Use the Components and Templates to create End Products where the End Product is sold to End Users. +- Use the Components and Templates to create End Products that are open source and freely available to End Users. + +You **cannot**: + +- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates. +- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product. +- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee. +- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc. + +### Example usage + +Examples of usage **allowed** by the license: + +- Creating a website for your company. +- Creating a website or web application for a client that will be owned by that client. +- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application. +- Creating a commercial self-hosted web application that is sold to end users for a one-time fee. +- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available. + +Examples of use **not allowed** by the license: + +- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly. +- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free. +- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI. +- Creating a theme or template using the components or templates and making it available either for sale or for free. +- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free. +- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects. + +### Team License Definitions + +Licensee is the business entity who has purchased a Team License. + +Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license. + +End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates. + +End User is a user of an End Product. + +Employee is a full-time or part-time employee of the Licensee. + +Contractor is an individual or business entity contracted to perform services for the Licensee. + +Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document. + +## Enforcement + +If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued. + +The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license. + +## Liability + +Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates. + +This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order. + +## Questions? + +Unsure which license you need, or unsure if your use case is covered by our licenses? + +Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions. diff --git a/README.md b/README.md index f568e8d..67f6d62 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,46 @@ -# Lysand Docs +# Protocol -## Contributing +Protocol is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org). -This site is built with [VitePress](https://github.com/vuejs/vitepress), and its content is written in Markdown format located in `docs`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request. +## Getting started -For local development, [bun](https://bun.sh/) is preferred as package manager and runtime: +To get started with this template, first install the npm dependencies: ```bash -bun i -bun docs:dev +npm install ``` + +Next, run the development server: + +```bash +npm run dev +``` + +Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. + +## Customizing + +You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files. + +## Global search + +This template includes a global search that's powered by the [FlexSearch](https://github.com/nextapps-de/flexsearch) library. It's available by clicking the search input or by using the `⌘K` shortcut. + +This feature requires no configuration, and works out of the box by automatically scanning your documentation pages to build its index. You can adjust the search parameters by editing the `/src/mdx/search.mjs` file. + +## License + +This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license). + +## Learn more + +To learn more about the technologies used in this site template, see the following resources: + +- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation +- [Next.js](https://nextjs.org/docs) - the official Next.js documentation +- [Headless UI](https://headlessui.dev) - the official Headless UI documentation +- [Framer Motion](https://www.framer.com/docs/) - the official Framer Motion documentation +- [MDX](https://mdxjs.com/) - the official MDX documentation +- [Algolia Autocomplete](https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete/) - the official Algolia Autocomplete documentation +- [FlexSearch](https://github.com/nextapps-de/flexsearch) - the official FlexSearch documentation +- [Zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) - the official Zustand documentation diff --git a/app/attachments/page.mdx b/app/attachments/page.mdx new file mode 100644 index 0000000..cd36364 --- /dev/null +++ b/app/attachments/page.mdx @@ -0,0 +1,363 @@ +export const metadata = { + title: 'Attachments', + description: + 'On this page, we’ll dive into the different attachment endpoints you can use to manage attachments programmatically.', +} + +# Attachments + +Attachments are how you share things in Protocol — they allow you to send all sorts of files to your contacts and groups. On this page, we'll dive into the different attachment endpoints you can use to manage attachments programmatically. We'll look at how to query, upload, update, and delete attachments. {{ className: 'lead' }} + +## The attachment model + +The attachment model contains all the information about the files you send to your contacts and groups, including the name, type, and size. + +### Properties + + + + Unique identifier for the attachment. + + + Unique identifier for the message associated with the attachment. + + + The filename for the attachment. + + + The URL for the attached file. + + + The MIME type of the attached file. + + + The file size of the attachment in bytes. + + + Timestamp of when the attachment was created. + + + +--- + +## List all attachments {{ tag: 'GET', label: '/v1/attachments' }} + + + + + This endpoint allows you to retrieve a paginated list of all your attachments (in a conversation if a conversation id is provided). By default, a maximum of ten attachments are shown per page. + + ### Optional attributes + + + + Limit to attachments from a given conversation. + + + Limit the number of attachments returned. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/attachments \ + -H "Authorization: Bearer {token}" \ + -d conversation_id="xgQQXg3hrtjh7AvZ" \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "Nc6yKKMpcxiiFxp6", + "message_id": "LoPsJaMcPBuFNjg1", + "filename": "Invoice_room_service__Plaza_Hotel.pdf", + "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", + "file_type": "application/pdf", + "file_size": 21352, + "created_at": 692233200 + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + +## Create an attachment {{ tag: 'POST', label: '/v1/attachments' }} + + + + + This endpoint allows you to upload a new attachment to a conversation. See the code examples for how to send the file to the Protocol API. + + ### Required attributes + + + + The file you want to add as an attachment. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/attachments \ + -H "Authorization: Bearer {token}" \ + -F file="../Invoice_room_service__Plaza_Hotel.pdf" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.create({ file }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.create(file=file) + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->create([ + 'file' => $file, + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "Nc6yKKMpcxiiFxp6", + "message_id": "LoPsJaMcPBuFNjg1", + "filename": "Invoice_room_service__Plaza_Hotel.pdf", + "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", + "file_type": "application/pdf", + "file_size": 21352, + "created_at": 692233200 + } + ``` + + + + +--- + +## Retrieve an attachment {{ tag: 'GET', label: '/v1/attachments/:id' }} + + + + + This endpoint allows you to retrieve an attachment by providing the attachment id. Refer to [the list](#the-attachment-model) at the top of this page to see which properties are included with attachment objects. + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.get('Nc6yKKMpcxiiFxp6') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.get("Nc6yKKMpcxiiFxp6") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->get('Nc6yKKMpcxiiFxp6'); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "Nc6yKKMpcxiiFxp6", + "message_id": "LoPsJaMcPBuFNjg1", + "filename": "Invoice_room_service__Plaza_Hotel.pdf", + "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", + "file_type": "application/pdf", + "file_size": 21352, + "created_at": 692233200 + } + ``` + + + + +--- + +## Update an attachment {{ tag: 'PUT', label: '/v1/attachments/:id' }} + + + + + This endpoint allows you to perform an update on an attachment. Currently, the only supported type of update is changing the filename. + + ### Optional attributes + + + + The new filename for the attachment. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ + -H "Authorization: Bearer {token}" \ + -d filename="Invoice_room_service__Plaza_Hotel_updated.pdf" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.update('Nc6yKKMpcxiiFxp6', { + filename: 'Invoice_room_service__Plaza_Hotel_updated.pdf', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.update("Nc6yKKMpcxiiFxp6", filename="Invoice_room_service__Plaza_Hotel_updated.pdf") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->update('Nc6yKKMpcxiiFxp6', [ + 'filename' => 'Invoice_room_service__Plaza_Hotel_updated.pdf', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "Nc6yKKMpcxiiFxp6", + "message_id": "LoPsJaMcPBuFNjg1", + "filename": "Invoice_room_service__Plaza_Hotel.pdf", + "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel_updated.pdf", + "file_type": "application/pdf", + "file_size": 21352, + "created_at": 692233200 + } + ``` + + + + +--- + +## Delete an attachment {{ tag: 'DELETE', label: '/v1/attachments/:id' }} + + + + + This endpoint allows you to delete attachments. Note: This will permanently delete the file. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.delete('Nc6yKKMpcxiiFxp6') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.delete("Nc6yKKMpcxiiFxp6") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->delete('Nc6yKKMpcxiiFxp6'); + ``` + + + + + diff --git a/app/authentication/page.mdx b/app/authentication/page.mdx new file mode 100644 index 0000000..625f0af --- /dev/null +++ b/app/authentication/page.mdx @@ -0,0 +1,41 @@ +export const metadata = { + title: 'Authentication', + description: + 'In this guide, we’ll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token.', +} + +# Authentication + +You'll need to authenticate your requests to access any of the endpoints in the Protocol API. In this guide, we'll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token — OAuth2 is the recommended way. {{ className: 'lead' }} + +## Basic authentication + +With basic authentication, you use your username and password to authenticate your HTTP requests. Unless you have a very good reason, you probably shouldn't use basic auth. Here's how to authenticate using cURL: + +```bash {{ title: 'Example request with basic auth' }} +curl https://api.protocol.chat/v1/conversations \ + -u username:password +``` + +Please don't commit your Protocol password to GitHub! + +## OAuth2 with bearer token + +The recommended way to authenticate with the Protocol API is by using OAuth2. When establishing a connection using OAuth2, you will need your access token — you will find it in the [Protocol dashboard](#) under API settings. Here's how to add the token to the request header using cURL: + +```bash {{ title: 'Example request with bearer token' }} +curl https://api.protocol.chat/v1/conversations \ + -H "Authorization: Bearer {token}" +``` + +Always keep your token safe and reset it if you suspect it has been compromised. + +## Using an SDK + +If you use one of our official SDKs, you won't have to worry about any of the above — fetch your access token from the [Protocol dashboard](#) under API settings, and the client library will take care of the rest. All the client libraries use OAuth2 behind the scenes. + +
+ +
diff --git a/app/contacts/page.mdx b/app/contacts/page.mdx new file mode 100644 index 0000000..b75afa9 --- /dev/null +++ b/app/contacts/page.mdx @@ -0,0 +1,394 @@ +export const metadata = { + title: 'Contacts', + description: + 'On this page, we’ll dive into the different contact endpoints you can use to manage contacts programmatically.', +} + +# Contacts + +As the name suggests, contacts are a core part of Protocol — the very reason Protocol exists is so you can have secure conversations with your contacts. On this page, we'll dive into the different contact endpoints you can use to manage contacts programmatically. We'll look at how to query, create, update, and delete contacts. {{ className: 'lead' }} + +## The contact model + +The contact model contains all the information about your contacts, such as their username, avatar, and phone number. It also contains a reference to the conversation between you and the contact and information about when they were last active on Protocol. + +### Properties + + + + Unique identifier for the contact. + + + The username for the contact. + + + The phone number for the contact. + + + The avatar image URL for the contact. + + + The contact display name in the contact list. By default, this is just the + username. + + + Unique identifier for the conversation associated with the contact. + + + Timestamp of when the contact was last active on the platform. + + + Timestamp of when the contact was created. + + + +--- + +## List all contacts {{ tag: 'GET', label: '/v1/contacts' }} + + + + + This endpoint allows you to retrieve a paginated list of all your contacts. By default, a maximum of ten contacts are shown per page. + + ### Optional attributes + + + + Limit the number of contacts returned. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/contacts \ + -H "Authorization: Bearer {token}" \ + -d active=true \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.contacts.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.contacts.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->contacts->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "WAz8eIbvDR60rouK", + "username": "FrankMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", + "display_name": null, + "conversation_id": "xgQQXg3hrtjh7AvZ", + "last_active_at": 705103200, + "created_at": 692233200 + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + +## Create a contact {{ tag: 'POST', label: '/v1/contacts' }} + + + + + This endpoint allows you to add a new contact to your contact list in Protocol. To add a contact, you must provide their Protocol username and phone number. + + ### Required attributes + + + + The username for the contact. + + + The phone number for the contact. + + + + ### Optional attributes + + + + The avatar image URL for the contact. + + + The contact display name in the contact list. By default, this is just the username. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/contacts \ + -H "Authorization: Bearer {token}" \ + -d username="FrankMcCallister" \ + -d phone_number="1-800-759-3000" \ + -d avatar_url="https://assets.protocol.chat/avatars/frank.jpg" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.contacts.create({ + username: 'FrankMcCallister', + phone_number: '1-800-759-3000', + avatar_url: 'https://assets.protocol.chat/avatars/frank.jpg', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.contacts.create( + username="FrankMcCallister", + phone_number="1-800-759-3000", + avatar_url="https://assets.protocol.chat/avatars/frank.jpg", + ) + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->contacts->create([ + 'username' => 'FrankMcCallister', + 'phone_number' => '1-800-759-3000', + 'avatar_url' => 'https://assets.protocol.chat/avatars/frank.jpg', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "WAz8eIbvDR60rouK", + "username": "FrankMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", + "display_name": null, + "conversation_id": "xgQQXg3hrtjh7AvZ", + "last_active_at": null, + "created_at": 692233200 + } + ``` + + + + +--- + +## Retrieve a contact {{ tag: 'GET', label: '/v1/contacts/:id' }} + + + + + This endpoint allows you to retrieve a contact by providing their Protocol id. Refer to [the list](#the-contact-model) at the top of this page to see which properties are included with contact objects. + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.contacts.get('WAz8eIbvDR60rouK') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.contacts.get("WAz8eIbvDR60rouK") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->contacts->get('WAz8eIbvDR60rouK'); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "WAz8eIbvDR60rouK", + "username": "FrankMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", + "display_name": null, + "conversation_id": "xgQQXg3hrtjh7AvZ", + "last_active_at": 705103200, + "created_at": 692233200 + } + ``` + + + + +--- + +## Update a contact {{ tag: 'PUT', label: '/v1/contacts/:id' }} + + + + + This endpoint allows you to perform an update on a contact. Currently, the only attribute that can be updated on contacts is the `display_name` attribute which controls how a contact appears in your contact list in Protocol. + + ### Optional attributes + + + + The contact display name in the contact list. By default, this is just the username. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ + -H "Authorization: Bearer {token}" \ + -d display_name="UncleFrank" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.contacts.update('WAz8eIbvDR60rouK', { + display_name: 'UncleFrank', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.contacts.update("WAz8eIbvDR60rouK", display_name="UncleFrank") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->contacts->update('WAz8eIbvDR60rouK', [ + 'display_name' => 'UncleFrank', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "WAz8eIbvDR60rouK", + "username": "FrankMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", + "display_name": "UncleFrank", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "last_active_at": 705103200, + "created_at": 692233200 + } + ``` + + + + +--- + +## Delete a contact {{ tag: 'DELETE', label: '/v1/contacts/:id' }} + + + + + This endpoint allows you to delete contacts from your contact list in Protocol. Note: This will also delete your conversation with the given contact. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.contacts.delete('WAz8eIbvDR60rouK') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.contacts.delete("WAz8eIbvDR60rouK") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->contacts->delete('WAz8eIbvDR60rouK'); + ``` + + + + + diff --git a/app/conversations/page.mdx b/app/conversations/page.mdx new file mode 100644 index 0000000..87fae52 --- /dev/null +++ b/app/conversations/page.mdx @@ -0,0 +1,407 @@ +export const metadata = { + title: 'Conversations', + description: + 'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.', +} + +# Conversations + +Conversations are an essential part of Protocol — they are the containers for the messages between you, your contacts, and groups. On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically. We'll look at how to query, create, update, and delete conversations. {{ className: 'lead' }} + +## The conversation model + +The conversation model contains all the information about the conversations between you and your contacts. In addition, conversations can also be group-based with more than one contact, they can have a pinned message, and they can be muted. + +### Properties + + + + Unique identifier for the conversation. + + + Unique identifier for the other contact in the conversation. + + + Unique identifier for the group that the conversation belongs to. + + + Unique identifier for the pinned message. + + + Whether or not the conversation has been pinned. + + + Whether or not the conversation has been muted. + + + Timestamp of when the conversation was last active. + + + Timestamp of when the conversation was last opened by the authenticated + user. + + + Timestamp of when the conversation was created. + + + Timestamp of when the conversation was archived. + + + +--- + +## List all conversations {{ tag: 'GET', label: '/v1/conversations' }} + + + + + This endpoint allows you to retrieve a paginated list of all your conversations. By default, a maximum of ten conversations are shown per page. + + ### Optional attributes + + + + Limit the number of conversations returned. + + + Only show conversations that are muted when set to `true`. + + + Only show conversations that are archived when set to `true`. + + + Only show conversations that are pinned when set to `true`. + + + Only show conversations for the specified group. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/conversations \ + -H "Authorization: Bearer {token}" \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.conversations.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.conversations.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->conversations->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "xgQQXg3hrtjh7AvZ", + "contact_id": "WAz8eIbvDR60rouK", + "group_id": null, + "pinned_message_id": null, + "is_pinned": false, + "is_muted": false, + "last_active_at": 705103200, + "last_opened_at": 705103200, + "created_at": 692233200, + "archived_at": null + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + +## Create a conversation {{ tag: 'POST', label: '/v1/conversations' }} + + + + + This endpoint allows you to add a new conversation between you and a contact or group. A contact or group id is required to create a conversation. + + ### Required attributes + + + + Unique identifier for the other contact in the conversation. + + + Unique identifier for the group that the conversation belongs to. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/conversations \ + -H "Authorization: Bearer {token}" \ + -d 'contact_id'="WAz8eIbvDR60rouK" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.conversations.create({ + contact_id: 'WAz8eIbvDR60rouK', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.conversations.create(contact_id="WAz8eIbvDR60rouK") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->conversations->create([ + 'contact_id' => 'WAz8eIbvDR60rouK', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "xgQQXg3hrtjh7AvZ", + "contact_id": "WAz8eIbvDR60rouK", + "group_id": null, + "pinned_message_id": null, + "is_pinned": false, + "is_muted": false, + "last_active_at": null, + "last_opened_at": null, + "created_at": 692233200, + "archived_at": null + } + ``` + + + + +--- + +## Retrieve a conversation {{ tag: 'GET', label: '/v1/conversations/:id' }} + + + + + This endpoint allows you to retrieve a conversation by providing the conversation id. Refer to [the list](#the-conversation-model) at the top of this page to see which properties are included with conversation objects. + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.conversations.get('xgQQXg3hrtjh7AvZ') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.conversations.get("xgQQXg3hrtjh7AvZ") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->conversations->get('xgQQXg3hrtjh7AvZ'); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "xgQQXg3hrtjh7AvZ", + "contact_id": "WAz8eIbvDR60rouK", + "group_id": null, + "pinned_message_id": null, + "is_pinned": false, + "is_muted": false, + "last_active_at": 705103200, + "last_opened_at": 705103200, + "created_at": 692233200, + "archived_at": null + } + ``` + + + + +--- + +## Update a conversation {{ tag: 'PUT', label: '/v1/conversations/:id' }} + + + + + This endpoint allows you to perform an update on a conversation. Examples of updates are pinning a message, muting or archiving the conversation, or pinning the conversation itself. + + ### Optional attributes + + + + Unique identifier for the pinned message. + + + Whether or not the conversation has been pinned. + + + Whether or not the conversation has been muted. + + + Timestamp of when the conversation was archived. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ + -H "Authorization: Bearer {token}" \ + -d 'is_muted'=true + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.conversations.update('xgQQXg3hrtjh7AvZ', { + is_muted: true, + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.conversations.update("xgQQXg3hrtjh7AvZ", is_muted=True) + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->conversations->update('xgQQXg3hrtjh7AvZ', [ + 'is_muted' => true, + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "xgQQXg3hrtjh7AvZ", + "contact_id": "WAz8eIbvDR60rouK", + "group_id": null, + "pinned_message_id": null, + "is_pinned": false, + "is_muted": true, + "last_active_at": 705103200, + "last_opened_at": 705103200, + "created_at": 692233200, + "archived_at": null + } + ``` + + + + +--- + +## Delete a conversation {{ tag: 'DELETE', label: '/v1/conversations/:id' }} + + + + + This endpoint allows you to delete your conversations in Protocol. Note: This will permanently delete the conversation and all its messages — archive it instead if you want to be able to restore it later. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.conversations.delete('xgQQXg3hrtjh7AvZ') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.conversations.delete("xgQQXg3hrtjh7AvZ") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->conversations->delete('xgQQXg3hrtjh7AvZ'); + ``` + + + + + diff --git a/app/errors/page.mdx b/app/errors/page.mdx new file mode 100644 index 0000000..15f070b --- /dev/null +++ b/app/errors/page.mdx @@ -0,0 +1,70 @@ +export const metadata = { + title: 'Errors', + description: + 'In this guide, we will talk about what happens when something goes wrong while you work with the API.', +} + +# Errors + +In this guide, we will talk about what happens when something goes wrong while you work with the API. Mistakes happen, and mostly they will be yours, not ours. Let's look at some status codes and error types you might encounter. {{ className: 'lead' }} + +You can tell if your request was successful by checking the status code when receiving an API response. If a response comes back unsuccessful, you can use the error type and error message to figure out what has gone wrong and do some rudimentary debugging (before contacting support). + + + Before reaching out to support with an error, please be aware that 99% of all + reported errors are, in fact, user errors. Therefore, please carefully check + your code before contacting Protocol support. + + +--- + +## Status codes + +Here is a list of the different categories of status codes returned by the Protocol API. Use these to understand if a request was successful. + + + + A 2xx status code indicates a successful response. + + + A 4xx status code indicates a client error — this means it's a _you_ + problem. + + + A 5xx status code indicates a server error — you won't be seeing these. + + + +--- + +## Error types + + + + + Whenever a request is unsuccessful, the Protocol API will return an error response with an error type and message. You can use this information to understand better what has gone wrong and how to fix it. Most of the error messages are pretty helpful and actionable. + + Here is a list of the two error types supported by the Protocol API — use these to understand what you have done wrong. + + + + This means that we made an error, which is highly speculative and unlikely. + + + This means that you made an error, which is much more likely. + + + + + + + ```bash {{ title: "Error response" }} + { + "type": "api_error", + "message": "No way this is happening!?", + "documentation_url": "https://protocol.chat/docs/errors/api_error" + } + ``` + + + diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2deafb7ba9477100832fca12b760676acd1cbb75 GIT binary patch literal 15086 zcmeI3PiPcZ9LL|1nuPi%L9C(@XXziPpqQ48$*CKmLa%x#s0VGOp*<<&;4xZg4~5>9 z-isdmgIGK%p#_U5_0I|g!6ft$@z6H5v{a3WpRbu&*KuYtZ66~io?hFm`h68u*T;u?IA*KtVUzhd|e_QSHg#gWo~#qbw=2|am=!NPyV z@C3eBoxH?g>AzwSS77If%0SDXv32^d82*HFa0Cv*rxtvI?k?C0-HnVT_2ZtOL0@Mj zm5UGHEHIWO-sbEN>b1)M+J5;RKSqyN20Cpc$cg`oMc?^fw6Xt+VG_33#DIPt@~1YJ zjsjG28S}mb&uAvzWiH zMgLBY{Fg6lQ~Q6r)QSF?9QiL_tk!YS*V?_-v$M>LG;QlQ&x2|EKG37i8n~PT|K-CQ zfnPR~dV}z53uBP&K^vdLx_%6okdK=fhiu7aL%s*G(9SwI4El{uznRWKn%uR~QJW7z zHV(nMz;PzVv*l3Mx0I!% zTsP(apPW;t6JH<+nNH^4QTAg=P8rjM)IO~u3`2FF7JIoCI~d>B#bSC{T*vMT_zq6N z<}7R^wTVp+CPDYdE%=b%y_>c_3VX6Y1+p*h!w6V?M<&IXbPYRU`|_b_`;%hq%ZH}y zFG{d4AB6S?m9xr;6!zuAG<*xY;iE;^!FB!S+L1Kw!8PHF368p0WL>v_INH`|6h;VP%{T8OJaC_n@zZ_LbY7vmf09 zO&EXI|1SUB{TF@vW&Qo5=dZi}@_qkpV2rdUD()So(Q>uDAtwElf8B5)L;F9-Mx1?> zbA1o!y|K|)!}ZnX-Xz<;ef7atpm|TYje5`3mfB2=J@5@Z&A~r|UqJ8J_1aZkwV}2m z=kzS5%>NA!VMdg}1J-9|Sd*DCrVO4>53*tYqdmbI#+cSAdZ7r_b&8nX$;6=bfqf90 z!!#3nP;V3H9aMQJ(^dy{JkT>#qYpJ7 zj~eT!GU?WipPozSE$U16rRNUkLvS7DAj+o7bWJ)#LH=sZ_Xhk7KfyTv8)ckTjWkT5 F`wWs{%d-Fg literal 0 HcmV?d00001 diff --git a/app/groups/page.mdx b/app/groups/page.mdx new file mode 100644 index 0000000..e304787 --- /dev/null +++ b/app/groups/page.mdx @@ -0,0 +1,448 @@ +export const metadata = { + title: 'Groups', + description: + 'On this page, we’ll dive into the different group endpoints you can use to manage groups programmatically.', +} + +# Groups + +Groups are where communities live in Protocol — they are a collection of contacts you're talking to all at once. On this page, we'll dive into the different group endpoints you can use to manage groups programmatically. We'll look at how to query, create, update, and delete groups. {{ className: 'lead' }} + +## The group model + +The group model contains all the information about your groups, including what contacts are in the group and the group's name, description, and avatar. + +### Properties + + + + Unique identifier for the group. + + + The name for the group. + + + The description for the group. + + + The avatar image URL for the group. + + + Unique identifier for the conversation that belongs to the group. + + + An array of contact objects that are members of the group. + + + Timestamp of when the group was created. + + + Timestamp of when the group was archived. + + + +--- + +## List all groups {{ tag: 'GET', label: '/v1/groups' }} + + + + + This endpoint allows you to retrieve a paginated list of all your groups. By default, a maximum of ten groups are shown per page. + + ### Optional attributes + + + + Limit the number of groups returned. + + + Only show groups that are archived when set to `true`. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/groups \ + -H "Authorization: Bearer {token}" \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.groups.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.groups.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->groups->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "l7cGNIBKZiNJ6wqF", + "name": "Plaza Hotel", + "description": "World-renowned.", + "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", + "conversation_id": "ZYjVAbCE9g5XRlra", + "contacts": [ + { + "username": "Hector" + // ... + }, + { + "username": "Cedric" + // ... + }, + { + "username": "Hester" + // ... + }, + { + "username": "Cliff" + // ... + } + ], + "created_at": 692233200, + "archived_at": null + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + +## Create a group {{ tag: 'POST', label: '/v1/groups' }} + + + + + This endpoint allows you to create a new group conversation between you and a group of your Protocol contacts. + + ### Required attributes + + + + The name for the group. + + + + ### Optional attributes + + + + The description for the group. + + + The avatar image URL for the group. + + + An array of contact objects that are members of the group. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/groups \ + -H "Authorization: Bearer {token}" \ + -d name="Plaza Hotel" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.groups.create({ + name: 'Plaza Hotel', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.groups.create(name="Plaza Hotel") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->groups->create([ + 'name' => 'Plaza Hotel', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "l7cGNIBKZiNJ6wqF", + "name": "Plaza Hotel", + "description": null, + "avatar_url": null, + "conversation_id": "ZYjVAbCE9g5XRlra", + "contacts": [], + "created_at": 692233200, + "archived_at": null + } + ``` + + + + +--- + +## Retrieve a group {{ tag: 'GET', label: '/v1/groups/:id' }} + + + + + This endpoint allows you to retrieve a group by providing the group id. Refer to [the list](#the-group-model) at the top of this page to see which properties are included with group objects. + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.groups.get('L7cGNIBKZiNJ6wqF') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.groups.get("L7cGNIBKZiNJ6wqF") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->groups->get('L7cGNIBKZiNJ6wqF'); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "l7cGNIBKZiNJ6wqF", + "name": "Plaza Hotel", + "description": "World-renowned.", + "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", + "conversation_id": "ZYjVAbCE9g5XRlra", + "contacts": [ + { + "username": "Hector" + // ... + }, + { + "username": "Cedric" + // ... + }, + { + "username": "Hester" + // ... + }, + { + "username": "Cliff" + // ... + } + ], + "created_at": 692233200, + "archived_at": null + } + ``` + + + + +--- + +## Update a group {{ tag: 'PUT', label: '/v1/groups/:id' }} + + + + + This endpoint allows you to perform an update on a group. Examples of updates are changing the name, description, and avatar or adding and removing contacts from the group. + + ### Optional attributes + + + + The new name for the group. + + + The new description for the group. + + + The new avatar image URL for the group. + + + An array of contact objects that are members of the group. + + + Timestamp of when the group was archived. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ + -H "Authorization: Bearer {token}" \ + -d description="The finest in New York." + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.groups.update('L7cGNIBKZiNJ6wqF', { + description: 'The finest in New York.', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.groups.update("L7cGNIBKZiNJ6wqF", description="The finest in New York.") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->groups->update('L7cGNIBKZiNJ6wqF', [ + 'description' => 'The finest in New York.', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "l7cGNIBKZiNJ6wqF", + "name": "Plaza Hotel", + "description": "The finest in New York.", + "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", + "conversation_id": "ZYjVAbCE9g5XRlra", + "contacts": [ + { + "username": "Hector" + // ... + }, + { + "username": "Cedric" + // ... + }, + { + "username": "Hester" + // ... + }, + { + "username": "Cliff" + // ... + } + ], + "created_at": 692233200, + "archived_at": null + }, + ``` + + + + +--- + +## Delete a group {{ tag: 'DELETE', label: '/v1/groups/:id' }} + + + + + This endpoint allows you to delete groups. Note: This will permanently delete the group, including the messages — archive it instead if you want to be able to restore it later. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.groups.delete('L7cGNIBKZiNJ6wqF') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.groups.delete("L7cGNIBKZiNJ6wqF") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->groups->delete('L7cGNIBKZiNJ6wqF'); + ``` + + + + + diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..e4e00db --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,43 @@ +import glob from "fast-glob"; +import type { Metadata } from "next"; + +import { Layout } from "../components/Layout"; +import type { Section } from "../components/SectionProvider"; +import { Providers } from "./providers"; + +import "@/styles/tailwind.css"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: { + template: "%s - Protocol API Reference", + default: "Protocol API Reference", + }, +}; + +export default async function RootLayout({ + children, +}: { + children: ReactNode; +}) { + const pages = await glob("**/*.mdx", { cwd: "app" }); + const allSectionsEntries = (await Promise.all( + pages.map(async (filename) => [ + `/${filename.replace(/(^|\/)page\.mdx$/, "")}`, + (await import(`./${filename}`)).sections, + ]), + )) as [string, Section[]][]; + const allSections = Object.fromEntries(allSectionsEntries); + + return ( + + + +
+ {children} +
+
+ + + ); +} diff --git a/app/messages/page.mdx b/app/messages/page.mdx new file mode 100644 index 0000000..e3cbca0 --- /dev/null +++ b/app/messages/page.mdx @@ -0,0 +1,441 @@ +export const metadata = { + title: 'Messages', + description: + 'On this page, we’ll dive into the different message endpoints you can use to manage messages programmatically.', +} + +# Messages + +Messages are what conversations are made of in Protocol — they are the basic building blocks of your conversations with your Protocol contacts. On this page, we'll dive into the different message endpoints you can use to manage messages programmatically. We'll look at how to query, send, update, and delete messages. {{ className: 'lead' }} + +## The message model + +The message model contains all the information about the messages and attachments you send to your contacts and groups, including how your contacts have reacted to them. + +### Properties + + + + Unique identifier for the message. + + + Unique identifier for the conversation the message belongs to. + + + The contact object for the contact who sent the message. + + + The message content. + + + An array of reaction objects associated with the message. + + + An array of attachment objects associated with the message. + + + Timestamp of when the message was read. + + + Timestamp of when the message was created. + + + Timestamp of when the message was last updated. + + + +--- + +## List all messages {{ tag: 'GET', label: '/v1/messages' }} + + + + + This endpoint allows you to retrieve a paginated list of all your messages (in a conversation if a conversation id is provided). By default, a maximum of ten messages are shown per page. + + ### Optional attributes + + + + Limit to messages from a given conversation. + + + Limit the number of messages returned. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/messages \ + -H "Authorization: Bearer {token}" \ + -d conversation_id=xgQQXg3hrtjh7AvZ \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.messages.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.messages.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->messages->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "SIuAFUNKdSYHZF2w", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "contact": { + "id": "WAz8eIbvDR60rouK", + "username": "KevinMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/buzzboy.jpg", + "last_active_at": 705103200, + "created_at": 692233200 + }, + "message": "It’s a nice night for a neck injury.", + "reactions": [], + "attachments": [], + "read_at": 705103200, + "created_at": 692233200, + "updated_at": 692233200 + }, + { + "id": "hSIhXBhNe8X1d8Et", + // .. + } + ] + } + ``` + + + + +--- + +## Send a message {{ tag: 'POST', label: '/v1/messages' }} + + + + + This endpoint allows you to send a new message to one of your conversations. + + ### Required attributes + + + + Unique identifier for the conversation the message belongs to. + + + The message content. + + + + ### Optional attributes + + + + An array of attachment objects associated with the message. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/messages \ + -H "Authorization: Bearer {token}" \ + -d conversation_id="xgQQXg3hrtjh7AvZ" \ + -d message="You’re what the French call ‘les incompetents.’" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.messages.send({ + conversation_id: 'xgQQXg3hrtjh7AvZ', + message: 'You’re what the French call ‘les incompetents.’', + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.messages.send( + conversation_id="xgQQXg3hrtjh7AvZ", + message="You’re what the French call ‘les incompetents.’", + ) + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->messages->send([ + 'conversation_id' => 'xgQQXg3hrtjh7AvZ', + 'message' => 'You’re what the French call ‘les incompetents.’', + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "gWqY86BMFRiH5o11", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "contact": { + "id": "inEIRvzjC6YLMX3o", + "username": "LinnieMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/linnie.jpg", + "last_active_at": 705103200, + "created_at": 692233200 + }, + "message": "You’re what the French call ‘les incompetents.’", + "reactions": [], + "attachments": [], + "read_at": null, + "created_at": 692233200, + "updated_at": null + } + ``` + + + + +--- + +## Retrieve a message {{ tag: 'GET', label: '/v1/messages/:id' }} + + + + + This endpoint allows you to retrieve a message by providing the message id. Refer to [the list](#the-message-model) at the top of this page to see which properties are included with message objects. + + + + + + + ```bash {{ title: 'cURL' }} + curl https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.messages.get('SIuAFUNKdSYHZF2w') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.messages.get("SIuAFUNKdSYHZF2w") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->messages->get('SIuAFUNKdSYHZF2w'); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "SIuAFUNKdSYHZF2w", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "contact": { + "id": "WAz8eIbvDR60rouK", + "username": "KevinMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/kevin.jpg", + "last_active_at": 705103200, + "created_at": 692233200 + }, + "message": "I’m traveling with my dad. He’s at a meeting. I hate meetings.", + "reactions": [], + "attachments": [], + "read_at": 705103200, + "created_at": 692233200, + "updated_at": 692233200 + } + ``` + + + + +--- + +## Update a message {{ tag: 'PUT', label: '/v1/messages/:id' }} + + + + + This endpoint allows you to perform an update on a message. Examples of updates are adding a reaction, editing the message, or adding an attachment. + + ### Optional attributes + + + + The message content. + + + An array of reaction objects associated with the message. + + + An array of attachment objects associated with the message. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ + -H "Authorization: Bearer {token}" \ + -d reactions[red_angry_face][]="KateMcCallister" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.messages.update('SIuAFUNKdSYHZF2w', { + reactions: { + red_angry_face: ['KateMcCallister'] + } + }) + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.messages.update("SIuAFUNKdSYHZF2w", + reactions={"red_angry_face": ["KateMcCallister"]}) + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->messages->update('SIuAFUNKdSYHZF2w', [ + 'reactions' => [ + 'red_angry_face' => ['KateMcCallister'], + ], + ]); + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "SIuAFUNKdSYHZF2w", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "contact": { + "id": "WAz8eIbvDR60rouK", + "username": "KevinMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/buzzboy.jpg", + "last_active_at": 705103200, + "created_at": 692233200 + }, + "message": "I'm not apologizing. I'd rather kiss a toilet seat.", + "reactions": [ + { + "red_angry_face": [ + "KateMcCallister" + ] + } + ], + "attachments": [], + "read_at": 705103200, + "created_at": 692233200, + "updated_at": 692233200 + } + ``` + + + + +--- + +## Delete a message {{ tag: 'DELETE', label: '/v1/messages/:id' }} + + + + + This endpoint allows you to delete messages from your conversations. Note: This will permanently delete the message. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ + -H "Authorization: Bearer {token}" + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.messages.delete('SIuAFUNKdSYHZF2w') + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.messages.delete("SIuAFUNKdSYHZF2w") + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->messages->delete('SIuAFUNKdSYHZF2w'); + ``` + + + + + diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..0ec5795 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,24 @@ +import { Button } from "../components/Button"; +import { HeroPattern } from "../components/HeroPattern"; + +export default function NotFound() { + return ( + <> + +
+

+ 404 +

+

+ Page not found +

+

+ Sorry, we couldn’t find the page you’re looking for. +

+ +
+ + ); +} diff --git a/app/page.mdx b/app/page.mdx new file mode 100644 index 0000000..d3999f7 --- /dev/null +++ b/app/page.mdx @@ -0,0 +1,43 @@ +import { Guides } from '@/components/Guides' +import { Resources } from '@/components/Resources' +import { HeroPattern } from '@/components/HeroPattern' + +export const metadata = { + title: 'API Documentation', + description: + 'Learn everything there is to know about the Protocol API and integrate Protocol into your product.', +} + +export const sections = [ + { title: 'Guides', id: 'guides' }, + { title: 'Resources', id: 'resources' }, +] + + + +# API Documentation + +Use the Protocol API to access contacts, conversations, group messages, and more and seamlessly integrate your product into the workflows of dozens of devoted Protocol users. {{ className: 'lead' }} + +
+ + +
+ +## Getting started {{ anchor: false }} + +To get started, create a new application in your [developer settings](#), then read about how to make requests for the resources you need to access using our HTTP APIs or dedicated client SDKs. When your integration is ready to go live, publish it to our [integrations directory](#) to reach the Protocol community. {{ className: 'lead' }} + +
+ +
+ + + + diff --git a/app/pagination/page.mdx b/app/pagination/page.mdx new file mode 100644 index 0000000..8bec705 --- /dev/null +++ b/app/pagination/page.mdx @@ -0,0 +1,63 @@ +export const metadata = { + title: 'Pagination', + description: + 'In this guide, we will look at how to work with paginated responses when querying the Protocol API', +} + +# Pagination + +In this guide, we will look at how to work with paginated responses when querying the Protocol API. By default, all responses limit results to ten. However, you can go as high as 100 by adding a `limit` parameter to your requests. If you are using one of the official Protocol API client libraries, you don't need to worry about pagination, as it's all being taken care of behind the scenes. {{ className: 'lead' }} + +When an API response returns a list of objects, no matter the amount, pagination is supported. In paginated responses, objects are nested in a `data` attribute and have a `has_more` attribute that indicates whether you have reached the end of the last page. You can use the `starting_after` and `endding_before` query parameters to browse pages. + +## Example using cursors + + + + + In this example, we request the page that starts after the conversation with id `s4WycXedwhQrEFuM`. As a result, we get a list of three conversations and can tell by the `has_more` attribute that we have reached the end of the resultset. + + + + The last ID on the page you're currently on when you want to fetch the next page. + + + The first ID on the page you're currently on when you want to fetch the previous page. + + + Limit the number of items returned. + + + + + + + ```bash {{ title: 'Manual pagination using cURL' }} + curl -G https://api.protocol.chat/v1/conversations \ + -H "Authorization: Bearer {token}" \ + -d starting_after="s4WycXedwhQrEFuM" \ + -d limit=10 + ``` + + ```json {{ title: 'Paginated response' }} + { + "has_more": false, + "data": [ + { + "id": "WAz8eIbvDR60rouK", + // ... + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + }, + { + "id": "fbwYwpi9C2ybt6Yb" + // ... + } + ] + } + ``` + + + diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 0000000..6742e48 --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { ThemeProvider, useTheme } from "next-themes"; +import { type ReactNode, useEffect } from "react"; + +function ThemeWatcher() { + const { resolvedTheme, setTheme } = useTheme(); + + useEffect(() => { + const media = window.matchMedia("(prefers-color-scheme: dark)"); + + function onMediaChange() { + const systemTheme = media.matches ? "dark" : "light"; + if (resolvedTheme === systemTheme) { + setTheme("system"); + } + } + + onMediaChange(); + media.addEventListener("change", onMediaChange); + + return () => { + media.removeEventListener("change", onMediaChange); + }; + }, [resolvedTheme, setTheme]); + + return null; +} + +export function Providers({ children }: { children: ReactNode }) { + return ( + + + {children} + + ); +} diff --git a/app/quickstart/page.mdx b/app/quickstart/page.mdx new file mode 100644 index 0000000..7bf8aea --- /dev/null +++ b/app/quickstart/page.mdx @@ -0,0 +1,98 @@ +export const metadata = { + title: 'Quickstart', + description: + 'This guide will get you all set up and ready to use the Protocol API. We’ll cover how to get started an API client and how to make your first API request.', +} + +# Quickstart + +This guide will get you all set up and ready to use the Protocol API. We'll cover how to get started using one of our API clients and how to make your first API request. We'll also look at where to go next to find all the information you need to take full advantage of our powerful REST API. {{ className: 'lead' }} + + + Before you can make requests to the Protocol API, you will need to grab your + API key from your dashboard. You find it under [Settings » API](#). + + +## Choose your client + +Before making your first API request, you need to pick which API client you will use. In addition to good ol' cURL HTTP requests, Protocol offers clients for JavaScript, Python, and PHP. In the following example, you can see how to install each client. + + + +```bash {{ title: 'cURL' }} +# cURL is most likely already installed on your machine +curl --version +``` + +```bash {{ language: 'js' }} +# Install the Protocol JavaScript SDK +npm install @example/protocol-api --save +``` + +```bash {{ language: 'python' }} +# Install the Protocol Python SDK +pip install protocol_api +``` + +```bash {{ language: 'php' }} +# Install the Protocol PHP SDK +composer require protocol/sdk +``` + + + +
+ +
+ +## Making your first API request + +After picking your preferred client, you are ready to make your first call to the Protocol API. Below, you can see how to send a GET request to the Conversations endpoint to get a list of all your conversations. In the cURL example, results are limited to ten conversations, the default page length for each client. + + + +```bash {{ title: 'cURL' }} +curl -G https://api.protocol.chat/v1/conversations \ + -H "Authorization: Bearer {token}" \ + -d limit=10 +``` + +```js +import ApiClient from '@example/protocol-api' + +const client = new ApiClient(token) + +await client.conversations.list() +``` + +```python +from protocol_api import ApiClient + +client = ApiClient(token) + +client.conversations.list() +``` + +```php +$client = new \Protocol\ApiClient($token); + +$client->conversations->list(); +``` + + + +
+ +
+ +## What's next? + +Great, you're now set up with an API client and have made your first request to the API. Here are a few links that might be handy as you venture further into the Protocol API: + +- [Grab your API key from the Protocol dashboard](#) +- [Check out the Conversations endpoint](/conversations) +- [Learn about the different error messages in Protocol](/errors) diff --git a/app/sdks/page.mdx b/app/sdks/page.mdx new file mode 100644 index 0000000..ec7acd1 --- /dev/null +++ b/app/sdks/page.mdx @@ -0,0 +1,17 @@ +import { Libraries } from '@/components/Libraries' + +export const metadata = { + title: 'Protocol SDKs', + description: + 'Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API.', +} + +export const sections = [ + { title: 'Official libraries', id: 'official-libraries' }, +] + +# Protocol SDKs + +The recommended way to interact with the Protocol API is by using one of our official SDKs. Today, Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API. {{ className: 'lead' }} + + diff --git a/app/webhooks/page.mdx b/app/webhooks/page.mdx new file mode 100644 index 0000000..d8a568d --- /dev/null +++ b/app/webhooks/page.mdx @@ -0,0 +1,172 @@ +export const metadata = { + title: 'Webhooks', + description: + 'In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol.', +} + +# Webhooks + +In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol. With webhooks, your app can know when something happens in Protocol, such as someone sending a message or adding a contact. {{ className: 'lead' }} + +## Registering webhooks + +To register a new webhook, you need to have a URL in your app that Protocol can call. You can configure a new webhook from the Protocol dashboard under [API settings](#). Give your webhook a name, pick the [events](#event-types) you want to listen for, and add your URL. + +Now, whenever something of interest happens in your app, a webhook is fired off by Protocol. In the next section, we'll look at how to consume webhooks. + +## Consuming webhooks + +When your app receives a webhook request from Protocol, check the `type` attribute to see what event caused it. The first part of the event type will tell you the payload type, e.g., a conversation, message, etc. + +```json {{ title: 'Example webhook payload' }} +{ + "id": "a056V7R7NmNRjl70", + "type": "conversation.updated", + "payload": { + "id": "WAz8eIbvDR60rouK" + // ... + } +} +``` + +In the example above, a conversation was `updated`, and the payload type is a `conversation`. + +
+ +
+ +--- + +## Event types + + + + + + + A new contact was created. + + + An existing contact was updated. + + + A contact was successfully deleted. + + + A new conversation was created. + + + An existing conversation was updated. + + + A conversation was successfully deleted. + + + A new message was created. + + + An existing message was updated. + + + A message was successfully deleted. + + + A new group was created. + + + An existing group was updated. + + + A group was successfully deleted. + + + A new attachment was created. + + + An existing attachment was updated. + + + An attachment was successfully deleted. + + + + + + + ```json {{ 'title': 'Example payload' }} + { + "id": "a056V7R7NmNRjl70", + "type": "message.updated", + "payload": { + "id": "SIuAFUNKdSYHZF2w", + "conversation_id": "xgQQXg3hrtjh7AvZ", + "contact": { + "id": "WAz8eIbvDR60rouK", + "username": "KevinMcCallister", + "phone_number": "1-800-759-3000", + "avatar_url": "https://assets.protocol.chat/avatars/kevin.jpg", + "last_active_at": 705103200, + "created_at": 692233200 + }, + "message": "I’m traveling with my dad. He’s at a meeting. I hate meetings.", + "reactions": [], + "attachments": [], + "read_at": 705103200, + "created_at": 692233200, + "updated_at": 692233200 + } + } + ``` + + + + +--- + +## Security + +To know for sure that a webhook was, in fact, sent by Protocol instead of a malicious actor, you can verify the request signature. Each webhook request contains a header named `x-protocol-signature`, and you can verify this signature by using your secret webhook key. The signature is an HMAC hash of the request payload hashed using your secret key. Here is an example of how to verify the signature in your app: + + + +```js +const signature = req.headers['x-protocol-signature'] +const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex') + +if (hash === signature) { + // Request is verified +} else { + // Request could not be verified +} +``` + +```python +from flask import request +import hashlib +import hmac + +signature = request.headers.get("x-protocol-signature") +hash = hmac.new(bytes(secret, "ascii"), bytes(payload, "ascii"), hashlib.sha256) + +if hash.hexdigest() == signature: + # Request is verified +else: + # Request could not be verified +``` + +```php +$signature = $request['headers']['x-protocol-signature']; +$hash = hash_hmac('sha256', $payload, $secret); + +if (hash_equals($hash, $signature)) { + // Request is verified +} else { + // Request could not be verified +} +``` + + + +If your generated signature matches the `x-protocol-signature` header, you can be sure that the request was truly coming from Protocol. It's essential to keep your secret webhook key safe — otherwise, you can no longer be sure that a given webhook was sent by Protocol. Don't commit your secret webhook key to GitHub! diff --git a/biome.json b/biome.json index ef6b12d..4c705e6 100644 --- a/biome.json +++ b/biome.json @@ -1,20 +1,91 @@ { - "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "organizeImports": { - "enabled": true, - "ignore": ["node_modules", "dist", "cache"] + "enabled": true }, "linter": { "enabled": true, "rules": { - "recommended": true - }, - "ignore": ["node_modules", "dist", "cache"] + "all": true, + "correctness": { + "noNodejsModules": "off" + }, + "complexity": { + "noExcessiveCognitiveComplexity": "off" + }, + "style": { + "noDefaultExport": "off", + "noParameterProperties": "off", + "noNamespaceImport": "off", + "useFilenamingConvention": "off", + "useNamingConvention": { + "level": "warn", + "options": { + "requireAscii": false, + "strictCase": false, + "conventions": [ + { + "selector": { + "kind": "typeProperty" + }, + "formats": [ + "camelCase", + "CONSTANT_CASE", + "PascalCase", + "snake_case" + ] + }, + { + "selector": { + "kind": "objectLiteralProperty", + "scope": "any" + }, + "formats": [ + "camelCase", + "CONSTANT_CASE", + "PascalCase", + "snake_case" + ] + }, + { + "selector": { + "kind": "classMethod", + "scope": "any" + }, + "formats": ["camelCase", "PascalCase"] + }, + { + "selector": { + "kind": "functionParameter", + "scope": "any" + }, + "formats": ["snake_case", "camelCase"] + } + ] + } + } + }, + "nursery": { + "noDuplicateElseIf": "warn", + "noDuplicateJsonKeys": "warn", + "noEvolvingTypes": "warn", + "noYodaExpression": "warn", + "useConsistentBuiltinInstantiation": "warn", + "useErrorMessage": "warn", + "useImportExtensions": "off", + "useThrowNewError": "warn" + } + } }, "formatter": { "enabled": true, "indentStyle": "space", - "indentWidth": 4, - "ignore": ["node_modules", "dist", "cache"] + "indentWidth": 4 + }, + "javascript": { + "globals": ["Bun"] + }, + "files": { + "ignore": ["node_modules", ".next", ".output"] } } diff --git a/bun.lockb b/bun.lockb index bd348151a9bda2041768098677357a1e8fb734e2..90d4c9c1a348f10b0eab6b6083fc172cbfc01a4f 100755 GIT binary patch literal 271604 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%0AK&H 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 a&G8FHK5i(s4-%N?ot3{NB=Y~t-~R!Rb&BHv literal 63252 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

}beoLnCm2FE(?L!GsTJ9C!iV zgU^^^0ky{jcJ<`R!(CzZG=_Amc=*A)Gkrg%;rTV#Wi*m=Ea+}P=FNG+RTQaj*Y>o zp1_7<(P6+YZ{XX@9cz`9Zbto3>0>}7k- zf^Uf2w*Oq(37FfhRvHQX-(kY?gcCg|9}%d?O@P|RQclDwc^2$|kqu}7(l{QEU%#)Z}0i$#y#BSumd>l5m z$@lKty;(z5X5XAJx>OAH&3X~BNhiUGPCNXhwl5{0aUW^svYvTU7ig$khm7h@z_yVL zvR&T^+6;_=HFcdJr6si|7~BeB8{idJtcs|Rw;_nliK-Ar>Lj2tB@KLq&?;Hi_MiJgR0TPZEam+gD6174 z8qHbOahc0_SFOV7{xJPzU0uILeAhD4QJ7Tmm<#H5e28^rsi0hxl?E?Wv}>h8ahfKB z2{lzj*0{3mFdF3x^PzKzP|CD9;1G5N^9zSNRpcwJG+3E8jE~KZrTVS0RJo;ImSuru zm0Mz2V+Sm&+!BjB2cUSdB@%Nz0I^a_9Ol-*VWpNh(v8HF0S+s5!V%L^$iLJBAjT?_SHxl6lm}kbghR9}W4OLr1y?9T8>lU?6#>GTa>MTtG>un2>~!#`3FtQMwcR<)T{2sswTp4JwL z&l8KFy-j*RJWm1psN6I9%XZ>%BUSqYFhilzWnp5lp{Rcqj8ao&Q5iq$owQQs6WNh@=l)-6G4cQovb0dg8N`DSUMv z0}Sp!{DN3A%F+Q{IgI*C-deb|?#HVe1^y}nUDL?Z46FPXNO(rMn6q7x<{Z%Ymw6-s%<$cSPkkZ%$8}X zf`k2ZoCIM$3jH3dm!-AkLc|*vzK2Mfv0@*#HU45^cCMKfl2(*CJ0@~ zaF6h0exOe3WC5}2Aje_l$*e=2H93fktpb^9ihRE&?h_X!Zyyt61`*VZ!0C6kd%H4} z8-d_91<}1}qgR`00mBn7%Wtk;nB0z(?DbZ2xC8E_(ipK!FYYrrVB{xH;Fk{~)b5*_ zU#bGYF9b0a{CJSYk>B&j1@5a2M=@4N2)S5n)KX{_6BgJe6I|*?t`+E;r3Kv3-Hi{I z_TZ2D=$lDGq?C(GqO!4vzq3a-rd+qP!E5> z57{F}F7XGZH?j?SIz&*m8L?IxhQl=5GWBtUE0LZb1PJWKL^baHZxvCHR|!>giGnbK zM1cHurC4uOnWSBGZJZ7U=tKod*Cu%yn@(ln6=R&jqJ7!<&6P|;8EOU15a{xlNA9PY zgr|>?s6*$y;?jbwIYx?=*f=@dTZ=U+NnM4?d*|S zBC6iblEHOYw`4WoRQuV`%-G%0@ya3t(3Ki2B+JbS+Q$)}$vN=s@96+17@!J1V-zk> zes729fh}vgmyA7L%|*3(H_fUwdsSAogb*8~R=ot7tmpfH7K7GB>^Ikt%qHwC-8bDc ze6U7cwJ}aR;Bs%=f-NiCgL2C#NM6+*kPU&tV>RvJ*hpJFmiE;SHmePdKiQon1GWa! zE^BqP6O{HttCJR#{IV5)+x5fai;DBl>u;)WU9U0Uyf*$FZTkD9^w&P=@7|R-|1ZD) E1K3V@$p8QV diff --git a/components/Banner.vue b/components/Banner.vue deleted file mode 100644 index fd74c25..0000000 --- a/components/Banner.vue +++ /dev/null @@ -1,19 +0,0 @@ - - \ No newline at end of file diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 0000000..88526e8 --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,82 @@ +import clsx from "clsx"; +import Link from "next/link"; +import type { ComponentPropsWithoutRef } from "react"; + +function ArrowIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +const variantStyles = { + primary: + "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300", + secondary: + "rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300", + filled: "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-500 dark:text-white dark:hover:bg-emerald-400", + outline: + "rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white", + text: "text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-500", +}; + +type ButtonProps = { + variant?: keyof typeof variantStyles; + arrow?: "left" | "right"; +} & ( + | ComponentPropsWithoutRef + | (ComponentPropsWithoutRef<"button"> & { href?: undefined }) +); + +export function Button({ + variant = "primary", + className, + children, + arrow, + ...props +}: ButtonProps) { + className = clsx( + "inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition", + variantStyles[variant], + className, + ); + + const arrowIcon = ( + + ); + + const inner = ( + <> + {arrow === "left" && arrowIcon} + {children} + {arrow === "right" && arrowIcon} + + ); + + if (typeof props.href === "undefined") { + return ( + + ); + } + + return ( + + {inner} + + ); +} diff --git a/components/Code.tsx b/components/Code.tsx new file mode 100644 index 0000000..680776b --- /dev/null +++ b/components/Code.tsx @@ -0,0 +1,393 @@ +"use client"; + +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; +import clsx from "clsx"; +import { + Children, + type ComponentPropsWithoutRef, + type ReactNode, + createContext, + isValidElement, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { create } from "zustand"; + +import { Tag } from "./Tag"; + +const languageNames: Record = { + js: "JavaScript", + ts: "TypeScript", + javascript: "JavaScript", + typescript: "TypeScript", + php: "PHP", + python: "Python", + ruby: "Ruby", + go: "Go", +}; + +function getPanelTitle({ + title, + language, +}: { + title?: string; + language?: string; +}) { + if (title) { + return title; + } + if (language && language in languageNames) { + return languageNames[language]; + } + return "Code"; +} + +function ClipboardIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function CopyButton({ code }: { code: string }) { + const [copyCount, setCopyCount] = useState(0); + const copied = copyCount > 0; + + useEffect(() => { + if (copyCount > 0) { + const timeout = setTimeout(() => setCopyCount(0), 1000); + return () => { + clearTimeout(timeout); + }; + } + }, [copyCount]); + + return ( + + ); +} + +function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) { + if (!(tag || label)) { + return null; + } + + return ( +

+ {tag && ( +
+ {tag} +
+ )} + {tag && label && ( + + )} + {label && ( + {label} + )} +
+ ); +} + +function CodePanel({ + children, + tag, + label, + code, +}: { + children: ReactNode; + tag?: string; + label?: string; + code?: string; +}) { + const child = Children.only(children); + + if (isValidElement(child)) { + tag = child.props.tag ?? tag; + label = child.props.label ?? label; + code = child.props.code ?? code; + } + + if (!code) { + throw new Error( + "`CodePanel` requires a `code` prop, or a child with a `code` prop.", + ); + } + + return ( +
+ +
+
+                    {children}
+                
+ +
+
+ ); +} + +function CodeGroupHeader({ + title, + children, + selectedIndex, +}: { + title: string; + children: ReactNode; + selectedIndex: number; +}) { + const hasTabs = Children.count(children) > 1; + + if (!(title || hasTabs)) { + return null; + } + + return ( +
+ {title && ( +

+ {title} +

+ )} + {hasTabs && ( + + {Children.map(children, (child, childIndex) => ( + + {getPanelTitle( + isValidElement(child) ? child.props : {}, + )} + + ))} + + )} +
+ ); +} + +function CodeGroupPanels({ + children, + ...props +}: ComponentPropsWithoutRef) { + const hasTabs = Children.count(children) > 1; + + if (hasTabs) { + return ( + + {Children.map(children, (child) => ( + + {child} + + ))} + + ); + } + + return {children}; +} + +function usePreventLayoutShift() { + const positionRef = useRef(null); + const rafRef = useRef(); + + useEffect(() => { + return () => { + if (typeof rafRef.current !== "undefined") { + window.cancelAnimationFrame(rafRef.current); + } + }; + }, []); + + return { + positionRef, + preventLayoutShift(callback: () => void) { + if (!positionRef.current) { + return; + } + + const initialTop = positionRef.current.getBoundingClientRect().top; + + callback(); + + rafRef.current = window.requestAnimationFrame(() => { + const newTop = + positionRef.current?.getBoundingClientRect().top ?? + initialTop; + window.scrollBy(0, newTop - initialTop); + }); + }, + }; +} + +const usePreferredLanguageStore = create<{ + preferredLanguages: string[]; + addPreferredLanguage: (language: string) => void; +}>()((set) => ({ + preferredLanguages: [], + addPreferredLanguage: (language) => + set((state) => ({ + preferredLanguages: [ + ...state.preferredLanguages.filter( + (preferredLanguage) => preferredLanguage !== language, + ), + language, + ], + })), +})); + +function useTabGroupProps(availableLanguages: string[]) { + const { preferredLanguages, addPreferredLanguage } = + usePreferredLanguageStore(); + const [selectedIndex, setSelectedIndex] = useState(0); + const activeLanguage = [...availableLanguages].sort( + (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a), + )[0]; + const languageIndex = availableLanguages.indexOf(activeLanguage); + const newSelectedIndex = + languageIndex === -1 ? selectedIndex : languageIndex; + if (newSelectedIndex !== selectedIndex) { + setSelectedIndex(newSelectedIndex); + } + + const { positionRef, preventLayoutShift } = usePreventLayoutShift(); + + return { + as: "div" as const, + ref: positionRef, + selectedIndex, + onChange: (newSelectedIndex: number) => { + preventLayoutShift(() => + addPreferredLanguage(availableLanguages[newSelectedIndex]), + ); + }, + }; +} + +const CodeGroupContext = createContext(false); + +export function CodeGroup({ + children, + title, + ...props +}: ComponentPropsWithoutRef & { title: string }) { + const languages = + Children.map(children, (child) => + getPanelTitle(isValidElement(child) ? child.props : {}), + ) ?? []; + const tabGroupProps = useTabGroupProps(languages); + const hasTabs = Children.count(children) > 1; + + const containerClassName = + "my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"; + const header = ( + + {children} + + ); + const panels = {children}; + + return ( + + {hasTabs ? ( + +
+ {header} + {panels} +
+
+ ) : ( +
+
+ {header} + {panels} +
+
+ )} +
+ ); +} + +export function Code({ children, ...props }: ComponentPropsWithoutRef<"code">) { + const isGrouped = useContext(CodeGroupContext); + + if (isGrouped) { + if (typeof children !== "string") { + throw new Error( + "`Code` children must be a string when nested inside a `CodeGroup`.", + ); + } + return ( + // biome-ignore lint/security/noDangerouslySetInnerHtml: + // biome-ignore lint/style/useNamingConvention: + + ); + } + + return {children}; +} + +export function Pre({ + children, + ...props +}: ComponentPropsWithoutRef) { + const isGrouped = useContext(CodeGroupContext); + + if (isGrouped) { + return children; + } + + return {children}; +} diff --git a/components/Features.vue b/components/Features.vue deleted file mode 100644 index 46edf53..0000000 --- a/components/Features.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - \ No newline at end of file diff --git a/components/Feedback.tsx b/components/Feedback.tsx new file mode 100644 index 0000000..0e65c40 --- /dev/null +++ b/components/Feedback.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Transition } from "@headlessui/react"; +import { + type ComponentPropsWithoutRef, + type ElementRef, + type FormEvent, + forwardRef, + useState, +} from "react"; + +function CheckIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function FeedbackButton( + props: Omit, "type" | "className">, +) { + return ( + + + {page.title} + + + ); +} + +function PageNavigation() { + const pathname = usePathname(); + const allPages = navigation.flatMap((group) => group.links); + const currentPageIndex = allPages.findIndex( + (page) => page.href === pathname, + ); + + if (currentPageIndex === -1) { + return null; + } + + const previousPage = allPages[currentPageIndex - 1]; + const nextPage = allPages[currentPageIndex + 1]; + + if (!(previousPage || nextPage)) { + return null; + } + + return ( +
+ {previousPage && ( +
+ +
+ )} + {nextPage && ( +
+ +
+ )} +
+ ); +} + +function XIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function GitHubIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function DiscordIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function SocialLink({ + href, + icon: Icon, + children, +}: { + href: string; + icon: ComponentType<{ className?: string }>; + children: ReactNode; +}) { + return ( + + {children} + + + ); +} + +function SmallPrint() { + return ( +
+

+ © Copyright {new Date().getFullYear()}. All rights + reserved. +

+
+ + Follow us on X + + + Follow us on GitHub + + + Join our Discord server + +
+
+ ); +} + +export function Footer() { + return ( +
+ + +
+ ); +} diff --git a/components/GridPattern.tsx b/components/GridPattern.tsx new file mode 100644 index 0000000..14fefc8 --- /dev/null +++ b/components/GridPattern.tsx @@ -0,0 +1,61 @@ +import { type ComponentPropsWithoutRef, useId } from "react"; + +export function GridPattern({ + width, + height, + x, + y, + squares, + ...props +}: ComponentPropsWithoutRef<"svg"> & { + width: number; + height: number; + x: string | number; + y: string | number; + squares: [x: number, y: number][]; +}) { + const patternId = useId(); + + return ( + + ); +} diff --git a/components/Guides.tsx b/components/Guides.tsx new file mode 100644 index 0000000..42fac2e --- /dev/null +++ b/components/Guides.tsx @@ -0,0 +1,58 @@ +import { Button } from "./Button"; +import { Heading } from "./Heading"; + +const guides = [ + { + href: "/authentication", + name: "Authentication", + description: "Learn how to authenticate your API requests.", + }, + { + href: "/pagination", + name: "Pagination", + description: "Understand how to work with paginated responses.", + }, + { + href: "/errors", + name: "Errors", + description: + "Read about the different types of errors returned by the API.", + }, + { + href: "/webhooks", + name: "Webhooks", + description: + "Learn how to programmatically configure webhooks for your app.", + }, +]; + +export function Guides() { + return ( +
+ + Guides + +
+ {guides.map((guide) => ( +
+

+ {guide.name} +

+

+ {guide.description} +

+

+ +

+
+ ))} +
+
+ ); +} diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..f12d60c --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,104 @@ +import clsx from "clsx"; +import { motion, useScroll, useTransform } from "framer-motion"; +import Link from "next/link"; +import { + type CSSProperties, + type ElementRef, + type ReactNode, + forwardRef, +} from "react"; + +import { Button } from "./Button"; +import { Logo } from "./Logo"; +import { + MobileNavigation, + useIsInsideMobileNavigation, +} from "./MobileNavigation"; +import { useMobileNavigationStore } from "./MobileNavigation"; +import { MobileSearch, Search } from "./Search"; +import { ThemeToggle } from "./ThemeToggle"; + +function TopLevelNavItem({ + href, + children, +}: { + href: string; + children: ReactNode; +}) { + return ( +
  • + + {children} + +
  • + ); +} + +export const Header = forwardRef, { className?: string }>( + function Header({ className }, ref) { + const { isOpen: mobileNavIsOpen } = useMobileNavigationStore(); + const isInsideMobileNavigation = useIsInsideMobileNavigation(); + + const { scrollY } = useScroll(); + const bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9]); + const bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8]); + + return ( + +
    + +
    + + + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + ); + }, +); diff --git a/components/Heading.tsx b/components/Heading.tsx new file mode 100644 index 0000000..9975c26 --- /dev/null +++ b/components/Heading.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useInView } from "framer-motion"; +import Link from "next/link"; +import { + type ComponentPropsWithoutRef, + type ReactNode, + useEffect, + useRef, +} from "react"; + +import { remToPx } from "../lib/remToPx"; +import { useSectionStore } from "./SectionProvider"; +import { Tag } from "./Tag"; + +function AnchorIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function Eyebrow({ tag, label }: { tag?: string; label?: string }) { + if (!(tag || label)) { + return null; + } + + return ( +
    + {tag && {tag}} + {tag && label && ( + + )} + {label && ( + {label} + )} +
    + ); +} + +function Anchor({ + id, + inView, + children, +}: { + id: string; + inView: boolean; + children: ReactNode; +}) { + return ( + + {inView && ( +
    +
    + +
    +
    + )} + {children} + + ); +} + +export function Heading({ + children, + tag, + label, + level, + anchor = true, + ...props +}: ComponentPropsWithoutRef<`h${Level}`> & { + id: string; + tag?: string; + label?: string; + level?: Level; + anchor?: boolean; +}) { + level = level ?? (2 as Level); + const Component = `h${level}` as "h2" | "h3"; + const ref = useRef(null); + const registerHeading = useSectionStore((s) => s.registerHeading); + + const inView = useInView(ref, { + margin: `${remToPx(-3.5)}px 0px 0px 0px`, + amount: "all", + }); + + useEffect(() => { + if (level === 2) { + registerHeading({ + id: props.id, + ref, + offsetRem: tag || label ? 8 : 6, + }); + } + }); + + return ( + <> + + + {anchor ? ( + + {children} + + ) : ( + children + )} + + + ); +} diff --git a/components/HeroPattern.tsx b/components/HeroPattern.tsx new file mode 100644 index 0000000..b81ff81 --- /dev/null +++ b/components/HeroPattern.tsx @@ -0,0 +1,32 @@ +import { GridPattern } from "./GridPattern"; + +export function HeroPattern() { + return ( +
    +
    +
    + +
    + +
    +
    + ); +} diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..b07de15 --- /dev/null +++ b/components/Layout.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { motion } from "framer-motion"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import type { ReactNode } from "react"; +import { Footer } from "./Footer"; +import { Header } from "./Header"; +import { Logo } from "./Logo"; +import { Navigation } from "./Navigation"; +import { type Section, SectionProvider } from "./SectionProvider"; + +export function Layout({ + children, + allSections, +}: { + children: ReactNode; + allSections: Record; +}) { + const pathname = usePathname(); + + return ( + +
    + +
    +
    + + + +
    +
    + +
    +
    +
    +
    {children}
    +
    +
    +
    +
    + ); +} diff --git a/components/Libraries.tsx b/components/Libraries.tsx new file mode 100644 index 0000000..faf5ac2 --- /dev/null +++ b/components/Libraries.tsx @@ -0,0 +1,89 @@ +import Image from "next/image"; + +import logoGo from "@/images/logos/go.svg"; +import logoNode from "@/images/logos/node.svg"; +import logoPhp from "@/images/logos/php.svg"; +import logoPython from "@/images/logos/python.svg"; +import logoRuby from "@/images/logos/ruby.svg"; +import { Button } from "./Button"; +import { Heading } from "./Heading"; + +const libraries = [ + { + href: "#", + name: "PHP", + description: + "A popular general-purpose scripting language that is especially suited to web development.", + logo: logoPhp, + }, + { + href: "#", + name: "Ruby", + description: + "A dynamic, open source programming language with a focus on simplicity and productivity.", + logo: logoRuby, + }, + { + href: "#", + name: "Node.js", + description: + "Node.js® is an open-source, cross-platform JavaScript runtime environment.", + logo: logoNode, + }, + { + href: "#", + name: "Python", + description: + "Python is a programming language that lets you work quickly and integrate systems more effectively.", + logo: logoPython, + }, + { + href: "#", + name: "Go", + description: + "An open-source programming language supported by Google with built-in concurrency.", + logo: logoGo, + }, +]; + +export function Libraries() { + return ( +
    + + Official libraries + +
    + {libraries.map((library) => ( +
    +
    +

    + {library.name} +

    +

    + {library.description} +

    +

    + +

    +
    + +
    + ))} +
    +
    + ); +} diff --git a/components/Logo.tsx b/components/Logo.tsx new file mode 100644 index 0000000..b23dcd8 --- /dev/null +++ b/components/Logo.tsx @@ -0,0 +1,16 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function Logo(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/MobileNavigation.tsx b/components/MobileNavigation.tsx new file mode 100644 index 0000000..73c43f0 --- /dev/null +++ b/components/MobileNavigation.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { + Dialog, + DialogPanel, + Transition, + TransitionChild, +} from "@headlessui/react"; +import { motion } from "framer-motion"; +import { usePathname, useSearchParams } from "next/navigation"; +import { + type ComponentPropsWithoutRef, + type MouseEvent, + Suspense, + createContext, + useContext, + useEffect, + useRef, +} from "react"; +import { create } from "zustand"; + +import { Header } from "./Header"; +import { Navigation } from "./Navigation"; + +function MenuIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function XIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +const IsInsideMobileNavigationContext = createContext(false); + +function MobileNavigationDialog({ + isOpen, + close, +}: { + isOpen: boolean; + close: () => void; +}) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const initialPathname = useRef(pathname).current; + const initialSearchParams = useRef(searchParams).current; + + useEffect(() => { + if ( + pathname !== initialPathname || + searchParams !== initialSearchParams + ) { + close(); + } + }, [pathname, searchParams, close, initialPathname, initialSearchParams]); + + function onClickDialog(event: MouseEvent) { + if (!(event.target instanceof HTMLElement)) { + return; + } + + const link = event.target.closest("a"); + if ( + link && + link.pathname + link.search + link.hash === + window.location.pathname + + window.location.search + + window.location.hash + ) { + close(); + } + } + + return ( + + + +
    + + + + +
    + + + + + + + + +
    +
    + ); +} + +export function useIsInsideMobileNavigation() { + return useContext(IsInsideMobileNavigationContext); +} + +export const useMobileNavigationStore = create<{ + isOpen: boolean; + open: () => void; + close: () => void; + toggle: () => void; +}>()((set) => ({ + isOpen: false, + open: () => set({ isOpen: true }), + close: () => set({ isOpen: false }), + toggle: () => set((state) => ({ isOpen: !state.isOpen })), +})); + +export function MobileNavigation() { + const isInsideMobileNavigation = useIsInsideMobileNavigation(); + const { isOpen, toggle, close } = useMobileNavigationStore(); + const ToggleIcon = isOpen ? XIcon : MenuIcon; + + return ( + + + {!isInsideMobileNavigation && ( + + + + )} + + ); +} diff --git a/components/Navigation.tsx b/components/Navigation.tsx new file mode 100644 index 0000000..7b3df97 --- /dev/null +++ b/components/Navigation.tsx @@ -0,0 +1,292 @@ +"use client"; + +import clsx from "clsx"; +import { AnimatePresence, motion, useIsPresent } from "framer-motion"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { type ComponentPropsWithoutRef, type ReactNode, useRef } from "react"; + +import { remToPx } from "../lib/remToPx"; +import { Button } from "./Button"; +import { useIsInsideMobileNavigation } from "./MobileNavigation"; +import { useSectionStore } from "./SectionProvider"; +import { Tag } from "./Tag"; + +interface NavGroup { + title: string; + links: Array<{ + title: string; + href: string; + }>; +} + +function useInitialValue(value: T, condition = true) { + const initialValue = useRef(value).current; + return condition ? initialValue : value; +} + +function TopLevelNavItem({ + href, + children, +}: { + href: string; + children: ReactNode; +}) { + return ( +
  • + + {children} + +
  • + ); +} + +function NavLink({ + href, + children, + tag, + active = false, + isAnchorLink = false, +}: { + href: string; + children: ReactNode; + tag?: string; + active?: boolean; + isAnchorLink?: boolean; +}) { + return ( + + {children} + {tag && ( + + {tag} + + )} + + ); +} + +function VisibleSectionHighlight({ + group, + pathname, +}: { + group: NavGroup; + pathname: string; +}) { + const [sections, visibleSections] = useInitialValue( + [ + useSectionStore((s) => s.sections), + useSectionStore((s) => s.visibleSections), + ], + useIsInsideMobileNavigation(), + ); + + const isPresent = useIsPresent(); + const firstVisibleSectionIndex = Math.max( + 0, + [{ id: "_top" }, ...sections].findIndex( + (section) => section.id === visibleSections[0], + ), + ); + const itemHeight = remToPx(2); + const height = isPresent + ? Math.max(1, visibleSections.length) * itemHeight + : itemHeight; + const top = + group.links.findIndex((link) => link.href === pathname) * itemHeight + + firstVisibleSectionIndex * itemHeight; + + return ( + + ); +} + +function ActivePageMarker({ + group, + pathname, +}: { + group: NavGroup; + pathname: string; +}) { + const itemHeight = remToPx(2); + const offset = remToPx(0.25); + const activePageIndex = group.links.findIndex( + (link) => link.href === pathname, + ); + const top = offset + activePageIndex * itemHeight; + + return ( + + ); +} + +function NavigationGroup({ + group, + className, +}: { + group: NavGroup; + className?: string; +}) { + // If this is the mobile navigation then we always render the initial + // state, so that the state does not change during the close animation. + // The state will still update when we re-open (re-render) the navigation. + const isInsideMobileNavigation = useIsInsideMobileNavigation(); + const [pathname, sections] = useInitialValue( + [usePathname(), useSectionStore((s) => s.sections)], + isInsideMobileNavigation, + ); + + const isActiveGroup = + group.links.findIndex((link) => link.href === pathname) !== -1; + + return ( +
  • + + {group.title} + +
    + + {isActiveGroup && ( + + )} + + + + {isActiveGroup && ( + + )} + +
      + {group.links.map((link) => ( + + + {link.title} + + + {link.href === pathname && + sections.length > 0 && ( + + {sections.map((section) => ( +
    • + + {section.title} + +
    • + ))} +
      + )} +
      +
      + ))} +
    +
    +
  • + ); +} + +export const navigation: NavGroup[] = [ + { + title: "Guides", + links: [ + { title: "Introduction", href: "/" }, + { title: "Quickstart", href: "/quickstart" }, + { title: "SDKs", href: "/sdks" }, + { title: "Authentication", href: "/authentication" }, + { title: "Pagination", href: "/pagination" }, + { title: "Errors", href: "/errors" }, + { title: "Webhooks", href: "/webhooks" }, + ], + }, + { + title: "Resources", + links: [ + { title: "Contacts", href: "/contacts" }, + { title: "Conversations", href: "/conversations" }, + { title: "Messages", href: "/messages" }, + { title: "Groups", href: "/groups" }, + { title: "Attachments", href: "/attachments" }, + ], + }, +]; + +export function Navigation(props: ComponentPropsWithoutRef<"nav">) { + return ( + + ); +} diff --git a/components/Prose.tsx b/components/Prose.tsx new file mode 100644 index 0000000..ee0de0a --- /dev/null +++ b/components/Prose.tsx @@ -0,0 +1,25 @@ +import clsx from "clsx"; +import type { ComponentPropsWithoutRef, ElementType } from "react"; + +export function Prose({ + as, + className, + ...props +}: Omit, "as" | "className"> & { + as?: T; + className?: string; +}) { + const Component = as ?? "div"; + + return ( + *)` is used to select all direct children without an increase in specificity like you'd get from just `& > *` + "[html_:where(&>*)]:mx-auto [html_:where(&>*)]:max-w-2xl [html_:where(&>*)]:lg:mx-[calc(50%-min(50%,theme(maxWidth.lg)))] [html_:where(&>*)]:lg:max-w-3xl", + )} + {...props} + /> + ); +} diff --git a/components/Resources.tsx b/components/Resources.tsx new file mode 100644 index 0000000..b0a18b9 --- /dev/null +++ b/components/Resources.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { + type MotionValue, + motion, + useMotionTemplate, + useMotionValue, +} from "framer-motion"; +import Link from "next/link"; + +import type { + ComponentPropsWithoutRef, + ComponentType, + MouseEvent, +} from "react"; +import { GridPattern } from "./GridPattern"; +import { Heading } from "./Heading"; +import { ChatBubbleIcon } from "./icons/ChatBubbleIcon"; +import { EnvelopeIcon } from "./icons/EnvelopeIcon"; +import { UserIcon } from "./icons/UserIcon"; +import { UsersIcon } from "./icons/UsersIcon"; + +interface Resource { + href: string; + name: string; + description: string; + icon: ComponentType<{ className?: string }>; + pattern: Omit< + ComponentPropsWithoutRef, + "width" | "height" | "x" + >; +} + +const resources: Resource[] = [ + { + href: "/contacts", + name: "Contacts", + description: + "Learn about the contact model and how to create, retrieve, update, delete, and list contacts.", + icon: UserIcon, + pattern: { + y: 16, + squares: [ + [0, 1], + [1, 3], + ], + }, + }, + { + href: "/conversations", + name: "Conversations", + description: + "Learn about the conversation model and how to create, retrieve, update, delete, and list conversations.", + icon: ChatBubbleIcon, + pattern: { + y: -6, + squares: [ + [-1, 2], + [1, 3], + ], + }, + }, + { + href: "/messages", + name: "Messages", + description: + "Learn about the message model and how to create, retrieve, update, delete, and list messages.", + icon: EnvelopeIcon, + pattern: { + y: 32, + squares: [ + [0, 2], + [1, 4], + ], + }, + }, + { + href: "/groups", + name: "Groups", + description: + "Learn about the group model and how to create, retrieve, update, delete, and list groups.", + icon: UsersIcon, + pattern: { + y: 22, + squares: [[0, 1]], + }, + }, +]; + +function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) { + return ( +
    + +
    + ); +} + +function ResourcePattern({ + mouseX, + mouseY, + ...gridProps +}: Resource["pattern"] & { + mouseX: MotionValue; + mouseY: MotionValue; +}) { + const maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`; + const style = { maskImage, WebkitMaskImage: maskImage }; + + return ( +
    +
    + +
    + + + + +
    + ); +} + +function Resource({ resource }: { resource: Resource }) { + const mouseX = useMotionValue(0); + const mouseY = useMotionValue(0); + + function onMouseMove({ + currentTarget, + clientX, + clientY, + }: MouseEvent) { + const { left, top } = currentTarget.getBoundingClientRect(); + mouseX.set(clientX - left); + mouseY.set(clientY - top); + } + + return ( +
    + +
    +
    + +

    + + + {resource.name} + +

    +

    + {resource.description} +

    +
    +
    + ); +} + +export function Resources() { + return ( +
    + + Resources + +
    + {resources.map((resource) => ( + + ))} +
    +
    + ); +} diff --git a/components/Search.tsx b/components/Search.tsx new file mode 100644 index 0000000..4d359eb --- /dev/null +++ b/components/Search.tsx @@ -0,0 +1,511 @@ +"use client"; + +import { + type AutocompleteApi, + type AutocompleteCollection, + type AutocompleteState, + createAutocomplete, +} from "@algolia/autocomplete-core"; +import { + Dialog, + DialogPanel, + Transition, + TransitionChild, +} from "@headlessui/react"; +import clsx from "clsx"; +import { useRouter } from "next/navigation"; +import { + type ComponentPropsWithoutRef, + type ElementRef, + Fragment, + type MouseEvent, + type KeyboardEvent as ReactKeyboardEvent, + Suspense, + type SyntheticEvent, + forwardRef, + useCallback, + useEffect, + useId, + useRef, + useState, +} from "react"; +import Highlighter from "react-highlight-words"; + +import type { Result } from "@/mdx/search.mjs"; +import { navigation } from "./Navigation"; + +type EmptyObject = Record; + +type Autocomplete = AutocompleteApi< + Result, + SyntheticEvent, + MouseEvent, + ReactKeyboardEvent +>; + +function useAutocomplete({ close }: { close: () => void }) { + const id = useId(); + const router = useRouter(); + const [autocompleteState, setAutocompleteState] = useState< + AutocompleteState | EmptyObject + >({}); + + function navigate({ itemUrl }: { itemUrl?: string }) { + if (!itemUrl) { + return; + } + + router.push(itemUrl); + + if ( + itemUrl === + window.location.pathname + + window.location.search + + window.location.hash + ) { + close(); + } + } + + const [autocomplete] = useState(() => + createAutocomplete< + Result, + SyntheticEvent, + MouseEvent, + ReactKeyboardEvent + >({ + id, + placeholder: "Find something...", + defaultActiveItemId: 0, + onStateChange({ state }) { + setAutocompleteState(state); + }, + shouldPanelOpen({ state }) { + return state.query !== ""; + }, + navigator: { + navigate, + }, + getSources({ query }) { + return import("@/mdx/search.mjs").then(({ search }) => { + return [ + { + sourceId: "documentation", + getItems() { + return search(query, { limit: 5 }); + }, + getItemUrl({ item }) { + return item.url; + }, + onSelect: navigate, + }, + ]; + }); + }, + }), + ); + + return { autocomplete, autocompleteState }; +} + +function SearchIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function NoResultsIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function LoadingIcon(props: ComponentPropsWithoutRef<"svg">) { + const id = useId(); + + return ( + + ); +} + +function HighlightQuery({ text, query }: { text: string; query: string }) { + return ( + + ); +} + +function SearchResult({ + result, + resultIndex, + autocomplete, + collection, + query, +}: { + result: Result; + resultIndex: number; + autocomplete: Autocomplete; + collection: AutocompleteCollection; + query: string; +}) { + const id = useId(); + + const sectionTitle = navigation.find((section) => + section.links.find((link) => link.href === result.url.split("#")[0]), + )?.title; + const hierarchy = [sectionTitle, result.pageTitle].filter( + (x): x is string => typeof x === "string", + ); + + return ( +
  • 0 && + "border-t border-zinc-100 dark:border-zinc-800", + )} + aria-labelledby={`${id}-hierarchy ${id}-title`} + {...autocomplete.getItemProps({ + item: result, + source: collection.source, + })} + > + + {hierarchy.length > 0 && ( + + )} +
  • + ); +} + +function SearchResults({ + autocomplete, + query, + collection, +}: { + autocomplete: Autocomplete; + query: string; + collection: AutocompleteCollection; +}) { + if (collection.items.length === 0) { + return ( +
    + +

    + Nothing found for{" "} + + ‘{query}’ + + . Please try again. +

    +
    + ); + } + + return ( +
      + {collection.items.map((result, resultIndex) => ( + + ))} +
    + ); +} + +const SearchInput = forwardRef< + ElementRef<"input">, + { + autocomplete: Autocomplete; + autocompleteState: AutocompleteState | EmptyObject; + onClose: () => void; + } +>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) { + const inputProps = autocomplete.getInputProps({ inputElement: null }); + + return ( +
    + + { + if ( + event.key === "Escape" && + !autocompleteState.isOpen && + autocompleteState.query === "" + ) { + // In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the + // bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI. + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + onClose(); + } else { + inputProps.onKeyDown(event); + } + }} + /> + {autocompleteState.status === "stalled" && ( +
    + +
    + )} +
    + ); +}); + +function SearchDialog({ + open, + setOpen, + className, +}: { + open: boolean; + setOpen: (open: boolean) => void; + className?: string; +}) { + const formRef = useRef>(null); + const panelRef = useRef>(null); + const inputRef = useRef>(null); + const { autocomplete, autocompleteState } = useAutocomplete({ + close() { + setOpen(false); + }, + }); + + useEffect(() => { + setOpen(false); + }, [setOpen]); + + useEffect(() => { + if (open) { + return; + } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === "k" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + setOpen(true); + } + } + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [open, setOpen]); + + return ( + autocomplete.setQuery("")}> + + +
    + + +
    + + +
    +
    + setOpen(false)} + /> +
    + {autocompleteState.isOpen && ( + + )} +
    + +
    +
    +
    +
    +
    +
    + ); +} + +function useSearchProps() { + const buttonRef = useRef>(null); + const [open, setOpen] = useState(false); + + return { + buttonProps: { + ref: buttonRef, + onClick() { + setOpen(true); + }, + }, + dialogProps: { + open, + setOpen: useCallback((open: boolean) => { + const { width = 0, height = 0 } = + buttonRef.current?.getBoundingClientRect() ?? {}; + if (!open || (width !== 0 && height !== 0)) { + setOpen(open); + } + }, []), + }, + }; +} + +export function Search() { + const [modifierKey, setModifierKey] = useState(); + const { buttonProps, dialogProps } = useSearchProps(); + + useEffect(() => { + setModifierKey( + /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl ", + ); + }, []); + + return ( +
    + + + + +
    + ); +} + +export function MobileSearch() { + const { buttonProps, dialogProps } = useSearchProps(); + + return ( +
    + + + + +
    + ); +} diff --git a/components/SectionProvider.tsx b/components/SectionProvider.tsx new file mode 100644 index 0000000..f891313 --- /dev/null +++ b/components/SectionProvider.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { + type ReactNode, + type RefObject, + createContext, + useContext, + useEffect, + useLayoutEffect, + useState, +} from "react"; +import { type StoreApi, createStore, useStore } from "zustand"; + +import { remToPx } from "../lib/remToPx"; + +export interface Section { + id: string; + title: string; + offsetRem?: number; + tag?: string; + headingRef?: RefObject; +} + +interface SectionState { + sections: Section[]; + visibleSections: string[]; + setVisibleSections: (visibleSections: string[]) => void; + registerHeading: ({ + id, + ref, + offsetRem, + }: { + id: string; + ref: RefObject; + offsetRem: number; + }) => void; +} + +function createSectionStore(sections: Section[]) { + return createStore()((set) => ({ + sections, + visibleSections: [], + setVisibleSections: (visibleSections) => + set((state) => + state.visibleSections.join() === visibleSections.join() + ? {} + : { visibleSections }, + ), + registerHeading: ({ id, ref, offsetRem }) => + set((state) => { + return { + sections: state.sections.map((section) => { + if (section.id === id) { + return { + ...section, + headingRef: ref, + offsetRem, + }; + } + return section; + }), + }; + }), + })); +} + +function useVisibleSections(sectionStore: StoreApi) { + const setVisibleSections = useStore( + sectionStore, + (s) => s.setVisibleSections, + ); + const sections = useStore(sectionStore, (s) => s.sections); + + useEffect(() => { + function checkVisibleSections() { + const { innerHeight, scrollY } = window; + const newVisibleSections: string[] = []; + + for ( + let sectionIndex = 0; + sectionIndex < sections.length; + sectionIndex++ + ) { + const { + id, + headingRef, + offsetRem = 0, + } = sections[sectionIndex]; + + if (!headingRef?.current) { + continue; + } + + const offset = remToPx(offsetRem); + const top = + headingRef.current.getBoundingClientRect().top + scrollY; + + if (sectionIndex === 0 && top - offset > scrollY) { + newVisibleSections.push("_top"); + } + + const nextSection = sections[sectionIndex + 1]; + const bottom = + (nextSection?.headingRef?.current?.getBoundingClientRect() + .top ?? Number.POSITIVE_INFINITY) + + scrollY - + remToPx(nextSection?.offsetRem ?? 0); + + if ( + (top > scrollY && top < scrollY + innerHeight) || + (bottom > scrollY && bottom < scrollY + innerHeight) || + (top <= scrollY && bottom >= scrollY + innerHeight) + ) { + newVisibleSections.push(id); + } + } + + setVisibleSections(newVisibleSections); + } + + const raf = window.requestAnimationFrame(() => checkVisibleSections()); + window.addEventListener("scroll", checkVisibleSections, { + passive: true, + }); + window.addEventListener("resize", checkVisibleSections); + + return () => { + window.cancelAnimationFrame(raf); + window.removeEventListener("scroll", checkVisibleSections); + window.removeEventListener("resize", checkVisibleSections); + }; + }, [setVisibleSections, sections]); +} + +const SectionStoreContext = createContext | null>(null); + +const useIsomorphicLayoutEffect = + typeof window === "undefined" ? useEffect : useLayoutEffect; + +export function SectionProvider({ + sections, + children, +}: { + sections: Section[]; + children: ReactNode; +}) { + const [sectionStore] = useState(() => createSectionStore(sections)); + + useVisibleSections(sectionStore); + + useIsomorphicLayoutEffect(() => { + sectionStore.setState({ sections }); + }, [sectionStore, sections]); + + return ( + + {children} + + ); +} + +export function useSectionStore(selector: (state: SectionState) => T) { + const store = useContext(SectionStoreContext); + return useStore(store as NonNullable, selector); +} diff --git a/components/Tag.tsx b/components/Tag.tsx new file mode 100644 index 0000000..c83859a --- /dev/null +++ b/components/Tag.tsx @@ -0,0 +1,58 @@ +import clsx from "clsx"; + +const variantStyles = { + small: "", + medium: "rounded-lg px-1.5 ring-1 ring-inset", +}; + +const colorStyles = { + emerald: { + small: "text-emerald-500 dark:text-emerald-400", + medium: "ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400", + }, + sky: { + small: "text-sky-500", + medium: "ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400", + }, + amber: { + small: "text-amber-500", + medium: "ring-amber-300 bg-amber-400/10 text-amber-500 dark:ring-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400", + }, + rose: { + small: "text-red-500 dark:text-rose-500", + medium: "ring-rose-200 bg-rose-50 text-red-500 dark:ring-rose-500/20 dark:bg-rose-400/10 dark:text-rose-400", + }, + zinc: { + small: "text-zinc-400 dark:text-zinc-500", + medium: "ring-zinc-200 bg-zinc-50 text-zinc-500 dark:ring-zinc-500/20 dark:bg-zinc-400/10 dark:text-zinc-400", + }, +}; + +const valueColorMap = { + GET: "emerald", + POST: "sky", + PUT: "amber", + DELETE: "rose", +} as Record; + +export function Tag({ + children, + variant = "medium", + color = valueColorMap[children] ?? "emerald", +}: { + children: keyof typeof valueColorMap; + variant?: keyof typeof variantStyles; + color?: keyof typeof colorStyles; +}) { + return ( + + {children} + + ); +} diff --git a/components/Team.vue b/components/Team.vue deleted file mode 100644 index 5c87218..0000000 --- a/components/Team.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx new file mode 100644 index 0000000..d74c77b --- /dev/null +++ b/components/ThemeToggle.tsx @@ -0,0 +1,46 @@ +import { useTheme } from "next-themes"; +import { type ComponentPropsWithoutRef, useEffect, useState } from "react"; + +function SunIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +function MoonIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + const otherTheme = resolvedTheme === "dark" ? "light" : "dark"; + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return ( + + ); +} diff --git a/components/icons/BellIcon.tsx b/components/icons/BellIcon.tsx new file mode 100644 index 0000000..4aaffc1 --- /dev/null +++ b/components/icons/BellIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function BellIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/BoltIcon.tsx b/components/icons/BoltIcon.tsx new file mode 100644 index 0000000..fec1340 --- /dev/null +++ b/components/icons/BoltIcon.tsx @@ -0,0 +1,13 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function BoltIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/BookIcon.tsx b/components/icons/BookIcon.tsx new file mode 100644 index 0000000..0de2b9a --- /dev/null +++ b/components/icons/BookIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function BookIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/CalendarIcon.tsx b/components/icons/CalendarIcon.tsx new file mode 100644 index 0000000..8591b83 --- /dev/null +++ b/components/icons/CalendarIcon.tsx @@ -0,0 +1,25 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function CalendarIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/CartIcon.tsx b/components/icons/CartIcon.tsx new file mode 100644 index 0000000..2d94c80 --- /dev/null +++ b/components/icons/CartIcon.tsx @@ -0,0 +1,17 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function CartIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ChatBubbleIcon.tsx b/components/icons/ChatBubbleIcon.tsx new file mode 100644 index 0000000..c891f18 --- /dev/null +++ b/components/icons/ChatBubbleIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ChatBubbleIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/CheckIcon.tsx b/components/icons/CheckIcon.tsx new file mode 100644 index 0000000..66c075c --- /dev/null +++ b/components/icons/CheckIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function CheckIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ChevronRightLeftIcon.tsx b/components/icons/ChevronRightLeftIcon.tsx new file mode 100644 index 0000000..759a6f5 --- /dev/null +++ b/components/icons/ChevronRightLeftIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ChevronRightLeftIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ClipboardIcon.tsx b/components/icons/ClipboardIcon.tsx new file mode 100644 index 0000000..0618e4c --- /dev/null +++ b/components/icons/ClipboardIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ClipboardIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/CogIcon.tsx b/components/icons/CogIcon.tsx new file mode 100644 index 0000000..d76a6ca --- /dev/null +++ b/components/icons/CogIcon.tsx @@ -0,0 +1,21 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function CogIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/CopyIcon.tsx b/components/icons/CopyIcon.tsx new file mode 100644 index 0000000..d6a5284 --- /dev/null +++ b/components/icons/CopyIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function CopyIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/DocumentIcon.tsx b/components/icons/DocumentIcon.tsx new file mode 100644 index 0000000..d3b1fad --- /dev/null +++ b/components/icons/DocumentIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function DocumentIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/EnvelopeIcon.tsx b/components/icons/EnvelopeIcon.tsx new file mode 100644 index 0000000..c9d93ee --- /dev/null +++ b/components/icons/EnvelopeIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function EnvelopeIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/FaceSmileIcon.tsx b/components/icons/FaceSmileIcon.tsx new file mode 100644 index 0000000..9c1ef2d --- /dev/null +++ b/components/icons/FaceSmileIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function FaceSmileIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/FolderIcon.tsx b/components/icons/FolderIcon.tsx new file mode 100644 index 0000000..45ea24f --- /dev/null +++ b/components/icons/FolderIcon.tsx @@ -0,0 +1,24 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function FolderIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/LinkIcon.tsx b/components/icons/LinkIcon.tsx new file mode 100644 index 0000000..bf05631 --- /dev/null +++ b/components/icons/LinkIcon.tsx @@ -0,0 +1,14 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function LinkIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ListIcon.tsx b/components/icons/ListIcon.tsx new file mode 100644 index 0000000..67b5086 --- /dev/null +++ b/components/icons/ListIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ListIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/MagnifyingGlassIcon.tsx b/components/icons/MagnifyingGlassIcon.tsx new file mode 100644 index 0000000..88fe4fd --- /dev/null +++ b/components/icons/MagnifyingGlassIcon.tsx @@ -0,0 +1,15 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function MagnifyingGlassIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/MapPinIcon.tsx b/components/icons/MapPinIcon.tsx new file mode 100644 index 0000000..80f6b30 --- /dev/null +++ b/components/icons/MapPinIcon.tsx @@ -0,0 +1,21 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function MapPinIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/PackageIcon.tsx b/components/icons/PackageIcon.tsx new file mode 100644 index 0000000..6b40664 --- /dev/null +++ b/components/icons/PackageIcon.tsx @@ -0,0 +1,18 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function PackageIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/PaperAirplaneIcon.tsx b/components/icons/PaperAirplaneIcon.tsx new file mode 100644 index 0000000..c26431f --- /dev/null +++ b/components/icons/PaperAirplaneIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function PaperAirplaneIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/PaperClipIcon.tsx b/components/icons/PaperClipIcon.tsx new file mode 100644 index 0000000..41eb143 --- /dev/null +++ b/components/icons/PaperClipIcon.tsx @@ -0,0 +1,14 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function PaperClipIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ShapesIcon.tsx b/components/icons/ShapesIcon.tsx new file mode 100644 index 0000000..63c2e61 --- /dev/null +++ b/components/icons/ShapesIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ShapesIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/ShirtIcon.tsx b/components/icons/ShirtIcon.tsx new file mode 100644 index 0000000..1478246 --- /dev/null +++ b/components/icons/ShirtIcon.tsx @@ -0,0 +1,13 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function ShirtIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/SquaresPlusIcon.tsx b/components/icons/SquaresPlusIcon.tsx new file mode 100644 index 0000000..0071df5 --- /dev/null +++ b/components/icons/SquaresPlusIcon.tsx @@ -0,0 +1,19 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function SquaresPlusIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/TagIcon.tsx b/components/icons/TagIcon.tsx new file mode 100644 index 0000000..5b48882 --- /dev/null +++ b/components/icons/TagIcon.tsx @@ -0,0 +1,21 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function TagIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/UserIcon.tsx b/components/icons/UserIcon.tsx new file mode 100644 index 0000000..bacc5e4 --- /dev/null +++ b/components/icons/UserIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function UserIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/icons/UsersIcon.tsx b/components/icons/UsersIcon.tsx new file mode 100644 index 0000000..934f348 --- /dev/null +++ b/components/icons/UsersIcon.tsx @@ -0,0 +1,30 @@ +import type { ComponentPropsWithoutRef } from "react"; + +export function UsersIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} diff --git a/components/mdx.tsx b/components/mdx.tsx new file mode 100644 index 0000000..cdac80e --- /dev/null +++ b/components/mdx.tsx @@ -0,0 +1,126 @@ +import clsx from "clsx"; +import Link from "next/link"; + +import type { ComponentPropsWithoutRef, ReactNode } from "react"; +import { Feedback } from "./Feedback"; +import { Heading } from "./Heading"; +import { Prose } from "./Prose"; + +export const a = Link; +// biome-ignore lint/performance/noBarrelFile: +export { Button } from "./Button"; +export { CodeGroup, Code as code, Pre as pre } from "./Code"; + +export function wrapper({ children }: { children: ReactNode }) { + return ( +
    + {children} +
    + +
    +
    + ); +} + +export const h2 = function H2( + props: Omit, "level">, +) { + return ; +}; + +function InfoIcon(props: ComponentPropsWithoutRef<"svg">) { + return ( + + ); +} + +export function Note({ children }: { children: ReactNode }) { + return ( +
    + +
    + {children} +
    +
    + ); +} + +export function Row({ children }: { children: ReactNode }) { + return ( +
    + {children} +
    + ); +} + +export function Col({ + children, + sticky = false, +}: { + children: ReactNode; + sticky?: boolean; +}) { + return ( +
    :first-child]:mt-0 [&>:last-child]:mb-0", + sticky && "xl:sticky xl:top-24", + )} + > + {children} +
    + ); +} + +export function Properties({ children }: { children: ReactNode }) { + return ( +
    +
      + {children} +
    +
    + ); +} + +export function Property({ + name, + children, + type, +}: { + name: string; + children: ReactNode; + type?: string; +}) { + return ( +
  • +
    +
    Name
    +
    + {name} +
    + {type && ( + <> +
    Type
    +
    + {type} +
    + + )} +
    Description
    +
    + {children} +
    +
    +
  • + ); +} diff --git a/docs/extensions.md b/docs/extensions.md deleted file mode 100644 index e4770c3..0000000 --- a/docs/extensions.md +++ /dev/null @@ -1,95 +0,0 @@ -# Protocol Extensions - -Lysand accommodates protocol extensions, which are enhancements to the protocol that are not part of the core protocol. These extensions are designed to augment the protocol with additional features and are namespaced. - -Protocol extensions can be incorporated into an object as follows: - -```json5 -{ - // ... - "extensions": { - "com.organization.name:key": "value" - } - // ... -} -``` - -The `extensions` field is an object that comprises a list of extensions. Each extension is a key-value pair, where the key represents the extension name, and the value signifies the extension value. - -The extension name **MUST** be a string that includes the reverse domain name of the organization that devised the extension, followed by a colon, and then the name of the extension. For instance, `com.example:extension_name`. - -The extension name **MUST** be unique within the organization namespace (i.e., it should be unique for each organization). - -The extension value **MAY** be any valid JSON value. The decision to implement extensions is at the discretion of the servers. - -For instance, a server might implement an extension that enables users to geotag an object. The extension name could be `org.geotagger:geotag`, and the extension value might be a string that contains the geotag. -```json5 -{ - // ... - "extensions": { - "org.geotagger:geotag": "40.7128° N, 74.0060° W" - } - // ... -} -``` - -Lysand strongly advocates that extensions are documented and standardized, and that servers refrain from implementing extensions that are not documented or standardized by their author. Moreover, official extensions of the Lysand protocol should take precedence over custom extensions. (Third-party extensions may be incorporated into the official spec if necessary). - -## Adding New Object Types - -Lysand supports the addition of new object types via extensions. This is beneficial for introducing new types of objects to the protocol, such as polls or events. - -Every new object type added **MUST** have `Extension` as its object type, and **MUST** have an `extension_type` field that contains the extension name of the object type. - -The extension name of the object type is formatted as follows: - -``` -com.organization.name:extension/Type -``` - -For instance, if a server wishes to add a new object type named `Poll`, the extension name would be `com.example:poll/Poll`. - -Custom types **MUST** commence with a capital letter, **MUST** be alphanumeric values (with PascalCase used instead of spaces) and **MUST** be unique across all extensions. - -Custom types **MUST** be unique within their organization namespace (i.e., it should be unique for each organization). - -An example is provided in the following object: -```json5 -{ - "type": "Extension", - "extension_type": "com.example:poll/Poll", - "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "question": "What is your favourite colour?", - "options": [ - "Red", - "Blue", - "Green" - ] -} -``` - -## Official Protocol Extensions - -Lysand has a selection of official extensions that are part of the core protocol. These extensions are standardized and documented, and servers **SHOULD** implement them if they implement the core protocol (however, they are not obligated to do so). - -These include: - -- [Custom Emojis](/extensions/custom-emojis) -- [Reactions](/extensions/reactions) -- [Polls](/extensions/polls) -- [Is Cat](/extensions/is-cat) -- [Server Endorsement](/extensions/server-endorsement) -- [Reports](/extensions/reports) - -## Types - -```typescript -// Specific extension types will extend from this -interface Extension extends Entity { - type: "Extension"; - extension_type: string; -} -``` \ No newline at end of file diff --git a/docs/extensions/custom-emojis.md b/docs/extensions/custom-emojis.md deleted file mode 100644 index ef9728e..0000000 --- a/docs/extensions/custom-emojis.md +++ /dev/null @@ -1,57 +0,0 @@ -# Custom Emojis - -For detailed information about the Custom Emoji type, refer to [Custom Emojis](../structures/custom-emoji). The implementation of the extension is as follows: - -```json5 -{ - // ... - "extensions": { - "org.lysand:custom_emojis": { - "emojis": [ - { - "name": "happy_face", - "url": { - "image/png": { - "content": "https://cdn.example.com/emojis/happy_face.png", - "content_type": "image/png" - } - } - }, - // ... - ] - } - } - // ... -} -``` - -In this context, the extension name is `org.lysand:custom_emojis`, and the extension value is an object that includes an array of emojis. - -## Utilizing Custom Emojis - -Clients are **required** to implement custom emojis in any text field where their presence is plausible. This includes, but is not limited to, status text, display names, alt text, and bio fields. However, this does not extend to an [Actor](../objects/actors)'s username, for instance. - -A custom emoji is represented within a text string as follows: -``` -:emoji_name: -``` - -For instance, to use the `happy_face` emoji, a user would type: -``` -:happy_face: -``` - -Clients are **required** to substitute the `:emoji_name:` with the corresponding inline emoji. If the client does not support custom emojis, it **should** display the `:emoji_name:` as it is. - -If the client supports Custom Emojis, but does not support a specific emoji that the user is attempting to use (such as with an incompatible MIME type), it **should** display the `:emoji_name:` as it is. - -When rendered as images, Custom Emojis **should** have appropriate alt text for accessibility. The alt text **should** be the alt text of the emoji, if it exists. If the emoji does not have an alt text, the alt text **should** be the name of the emoji. - -### Styling Custom Emojis - -If the styling system, such as CSS, supports it, clients **should** style the emoji to match the height of the text but allow it to take as much width as necessary, instead of treating it as a square and potentially distorting the image. - -Example in HTML: -```html -Hello, world! A happy face emoji. -``` \ No newline at end of file diff --git a/docs/extensions/events.md b/docs/extensions/events.md deleted file mode 100644 index ea0ed86..0000000 --- a/docs/extensions/events.md +++ /dev/null @@ -1,5 +0,0 @@ -# Events - -With the Events extension, users can create events. This is useful for creating gatherings, such as meetups or parties. - -This extension is planned but not yet drafted. \ No newline at end of file diff --git a/docs/extensions/interactivity.md b/docs/extensions/interactivity.md deleted file mode 100644 index 2a8cf1b..0000000 --- a/docs/extensions/interactivity.md +++ /dev/null @@ -1,10 +0,0 @@ -# Interactivity - -> [!WARNING] -> This extension is a work in progress and is not to be used in any production system. The specification is subject to change. - -On platforms like Discord, users can interact with messages with custom fields like buttons or dropdowns. This extension allows you to define these fields in your messages. - -![Discord Buttons in action](/assets/discord-buttons.webp) - -... \ No newline at end of file diff --git a/docs/extensions/is-cat.md b/docs/extensions/is-cat.md deleted file mode 100644 index 9e06169..0000000 --- a/docs/extensions/is-cat.md +++ /dev/null @@ -1,27 +0,0 @@ -# IsCat - -> [!NOTE] -> This is a **light-hearted** extension designed for amusement and not intended for serious application. - -> [!WARNING] -> This extension may be superseded by the upcoming [Vanity Profiles](./vanity) extension. - -The IsCat feature allows users to communicate their cat status to others, similar to Misskey's "IsCat" feature. - -A user, referred to as an Actor, can specify their feline status using the following field: - -```json5 -{ - "type": "User", - // ... - "extensions": { - "org.lysand:is_cat": { - "cat": true, - // Potential additional fields - // "dog": true - } - } -} -``` - -Clients **SHOULD** display an appropriate graphic to signify a user's cat status, such as adding cat ears to the user's avatar. \ No newline at end of file diff --git a/docs/extensions/microblogging.md b/docs/extensions/microblogging.md deleted file mode 100644 index 4f894ed..0000000 --- a/docs/extensions/microblogging.md +++ /dev/null @@ -1,66 +0,0 @@ -# Microblogging - -> [!WARNING] -> -> Before Lysand 3.0, microblogging was directly integrated into the core spec. As of Lysand 3.0, microblogging has been moved to an extension, as part of a larger modularization effort. This document describes the new microblogging extension. - -The Microblogging extension allows users to perform certain tasks related to microblogging, such as "boosting" (reposting) posts. - -## Announce - -The `Announce` action signifies a user's intent to broadcast or share an object with their followers. This action is analogous to the "retweet" function on Twitter. - - -`Announce`s can of course be deleted ("unboosting") with a classic [Undo](../objects/undo) object. - -Here's an example of an `Announce` action: - -```json5 -{ - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "type": "Extension", - "extension_type": "org.lysand:microblogging/Announce", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19" -} -``` - -### Fields - -#### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](../objects/actors) who initiated the action. - -#### Object - -| Name | Type | Required | -| :----- | :----- | :------- | -| object | String | Yes | - -URI of the object being announced. Must be of type [Note](../objects/note) - -#### Implementation - -When a [Note](../objects/note) object is announced, the client **SHOULD** display the original note with an indicator that it has been announced. The client **SHOULD** also display the number of times the note has been announced, such as a number next to a small icon like such on [Mastodon](https://joinmastodon.org/): - -![Bottom graphics of a Mastodon post, including a "boosting" icon with numbers next to it](/assets/boosting.png) - -Furthermore, users should be notified when their notes are announced by other users. - - -## Types - -```typescript -interface Announce extends Extension { - extension_type: "org.lysand:microblogging/Announce"; - author: string; - object: string; -} -``` - diff --git a/docs/extensions/migration.md b/docs/extensions/migration.md deleted file mode 100644 index d7591ed..0000000 --- a/docs/extensions/migration.md +++ /dev/null @@ -1,47 +0,0 @@ -# Migration - -Sometimes, users may wish to move from one instance to another. This could be due to a change in administration, a desire to be closer to friends, or any other reason. This document outlines an extension to make the process of moving instances easier. - -## User migrations - -The following object is used to represent a user migration: - -```json5 -{ - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "type": "Extension", - "extension_type": "org.lysand:migration/Migration", - "author": "https://example.com/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "uri": "https://example.com/actions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "created_at": "2021-01-01T00:00:00.000Z", - "destination": "https://otherinstance.social/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", -} -``` - -### Fields - -#### Author - -| Name | Type | Required | -| :----- | :--- | :------- | -| author | URI | Yes | - -URI of the [Actor](../objects/actors) who initiated the action. This Actor will be the user migrating. - -#### Destination - -| Name | Type | Required | -| :---------- | :--- | :------- | -| destination | URI | Yes | - -URI of the user's new account. - -### Implementation - -When an instance receives a `Migration` object, the client **SHOULD** display a notification to all followers of the migrating user. This notification **SHOULD** include a link to the user's new account. - -Furthermore, all following relationships of the migrating user **SHOULD** be transferred to the new account. This includes followers, following, and any other relationship that may exist. The old account **SHOULD** be marked as inactive and display a message indicating that the user has migrated to a new account. - -#### Server Actors - -If the user in question is a server actor, then it should be considered that the entire instance is migrating to a new address. In this case, the above process should be applied to all users on the instance. \ No newline at end of file diff --git a/docs/extensions/polls.md b/docs/extensions/polls.md deleted file mode 100644 index eb3b7e9..0000000 --- a/docs/extensions/polls.md +++ /dev/null @@ -1,249 +0,0 @@ -# Polls - -The Polls extension enables users to generate polls/surveys, a valuable tool for soliciting feedback or opinions from users, such as "What is your preferred color?". - -Polls are incorporated as a new field under the [Note](../objects/note) Extensions, named `polls`. This field is an object that encapsulates the poll details. - -```json5 -{ - "id": "f08a124e-fe90-439e-8be4-15a428a72a19", - "type": "Note", - // ... - "extensions": { - "org.lysand:polls": { - "poll": { - "options": [ - { - "text/plain": { - "content": "Red" - } - }, - { - "text/plain": { - "content": "Blue" - } - }, - { - "text/plain": { - "content": "Green" - } - } - ], - "votes": [ - 9, - 5, - 0 - ], - "multiple_choice": false, - "expires_at": "2021-01-04T00:00:00.000Z" - } - } - } - // ... -} -``` - -The fields are explained below. - -> [!NOTE] -> There is no `question` field, as it is presumed that the question will be included in the `contents` field of the associated [Note](../objects/note). Clients are anticipated to render surveys adjacent to the contents of the [Note](../objects/note) itself. - -## Fields - -### Options - -| Name | Type | Required | -| :------ | :--------------------- | :------- | -| options | Array of ContentFormat | Yes | - -Displays the various options users can vote for. - -**MUST** contain at least 2 options, but does not have an upper limit for the number of options. - -> [!NOTE] -> Servers should limit the number of options to a reasonable number, preferably in a configurable manner, such as 40. This is to prevent abuse of the protocol by sending a large number of options, as they are not paginated. - -### Votes - -| Name | Type | Required | -| :---- | :--------------- | :------- | -| votes | Array of Integer | Yes | - -Contains the number of votes cast for each option. The index of the array corresponds to the index of the option in the `options` array. - -Votes should not be public: the server should hide the users that casted votes and only show the total amount. - -### Multiple Choice - -| Name | Type | Required | -| :-------------- | :------ | :------- | -| multiple_choice | Boolean | No | - -Indicates whether the poll is multiple choice. If true, users can vote for multiple options. If false, users can only vote for one option. - -If not provided, it is assumed that the poll is not multiple choice. - -### Expiration - -| Name | Type | Required | -| :--------- | :----- | :------- | -| expires_at | String | Yes | - -The date and time when the poll expires. After this time, the poll is closed and no more votes can be cast. - -Clients **SHOULD** display the time remaining until the poll expires. - -### Integration With Custom Emojis - -If you implement both the Polls and the [Custom Emojis](./custom-emojis) extensions, you can use the Custom Emojis extension to add emojis to poll options. - -Example: -```json5 -{ - // ... - "extensions": { - "org.lysand:polls": { - "poll": { - "options": [ - { - "text/plain": { - "content": "Red :red:" - } - }, - { - "text/plain": { - "content": "Blue :blue:" - } - }, - { - "text/plain": { - "content": "Green :green:" - } - } - ], - "votes": [ - 9, - 5, - 0 - ], - "multiple_choice": false, - "expires_at": "2021-01-04T00:00:00.000Z" - } - }, - "org.lysand:custom_emojis": { - "emojis": [ - { - "name": "red", - "url": { - "image/webp": { - "content": "https://cdn.example.com/emojis/red.webp" - } - } - }, - { - "name": "blue", - "url": { - "image/webp": { - "content": "https://cdn.example.com/emojis/blue.webp" - } - } - }, - { - "name": "green", - "url": { - "image/webp": { - "content": "https://cdn.example.com/emojis/green.webp" - } - } - } - ] - } - } - // ... -} -``` - -When rendering the poll options, clients **SHOULD** display emojis as recommended by the [Custom Emojis](./custom-emojis) extension. - -### Poll Results - -Clients **SHOULD** display poll results as a percentage of votes. For example, if 10 users voted for the first option, and 5 users voted for the second option, the first option should be displayed as 66.67%, and the second option should be displayed as 33.33%. (with the third option being 0%) - -Clients **SHOULD** display the number of votes for each option, and the total number of votes. - -### Sending Votes - -Clients **SHOULD** allow users to vote on polls. When a user votes on a poll, the client **MUST** send a `POST` request to the poll's [Note](../objects/note) URI with the following JSON object in the body: - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:polls/Vote", - "id": "6b1586cf-1f83-4d85-8d70-a5dc9f213b3e", - "uri": "https://example.com/actions/6b1586cf-1f83-4d85-8d70-a5dc9f213b3e", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "poll": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "option": 0 -} -``` - -The `option` field **MUST** be the index of the option in the `options` array that the user is voting for. - -In return, the server **MUST** respond with a `200 OK` response code, and a JSON object in the body, unless there is an error. This JSON object **MUST** be a valid `VoteResult` object. - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:polls/VoteResult", - "id": "d6eb84ea-cd13-43f9-9c54-01244da8e5e3", - "uri": "https://example.com/actions/d6eb84ea-cd13-43f9-9c54-01244da8e5e3", - "poll": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "votes": [ - 10, - 5, - 0 - ] -} -``` - -Each number in the `votes` array corresponds to the number of votes for each option. The index of the array corresponds to the index of the poll option in the original Poll object. - -If the poll is closed, the server **MUST** respond with a `403 Forbidden` response code. - -The total amount of votes can be calculated by summing the `votes` array. - -This amount **MUST** include the user's vote, and **SHOULD** be displayed to the user after voting. - -### Poll Events - -When a poll ends, a user that has voted in it **SHOULD** be notified of the results by the server. - -The server **MAY** send a `GET` request to the poll's Publication URI to update its internal database. - -## Types - -```typescript -interface Poll extends Extension { - extension_type: "org.lysand:polls/Poll"; - options: ContentFormat[]; - votes: number[]; // unsigned 64-bit integer - multiple_choice?: boolean; - expires_at: string; -} -``` - -```typescript -interface Vote extends Extension { - extension_type: "org.lysand:polls/Vote"; - poll: string; - option: number; // unsigned 64-bit integer -} -``` - -```typescript -interface VoteResult extends Extension { - extension_type: "org.lysand:polls/VoteResult"; - poll: string; - votes: number[]; // unsigned 64-bit integer -} -``` diff --git a/docs/extensions/reactions.md b/docs/extensions/reactions.md deleted file mode 100644 index 37bc253..0000000 --- a/docs/extensions/reactions.md +++ /dev/null @@ -1,151 +0,0 @@ -# Emoji Reactions - -The Emoji Reactions extension allows users to express their responses to objects using emojis, similar to the functionality provided by platforms like Facebook and Discord. - -Here's an example of a reaction to a post using this extension: - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:reactions/Reaction", - "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "content": "😀", -} -``` -## Fields - -### Type - -The `type` field **MUST BE** `Extension`. - -The `extension_type` field **MUST** be `org.lysand:reactions/Reaction`. - -### Object - -| Name | Type | Required | -| :----- | :----- | :------- | -| object | String | Yes | - -URI of the object that the user is reacting to. This **MUST** be a [Note](../objects/note) object. - -### Content - -| Name | Type | Required | -| :------ | :----- | :------- | -| content | String | Yes | - -Emoji that the user is reacting with. - -Clients **SHOULD** check if the value of `content` is an emoji: if it is not an emoji and instead is text, depending on which other extensions are implemented, it **MAY** be a [Custom Emoji](./custom-emojis). - -> [!NOTE] -> If this field is not recognized as an emoji or [Custom Emoji](./custom-emojis), the whole Reaction object can be discarded as it is invalid. -> -> Please see [Reactions With Custom Emojis](#reactions-with-custom-emojis) for more information about custom emoji reactions. - - -### Retrieving Reactions - -Clients can retrieve reactions to an object by sending a `GET` request to the reaction [Collection](../structures/collection)'s URI. - -The URI of the reaction [Collection](../structures/collection) **MUST** be specified as follows, inside a [Note](../objects/note): -```json5 -{ - // ... - "extensions": { - "org.lysand:reactions": { - "reactions": "https://example.com/notes/f08a124e-fe90-439e-8be4-15a428a72a19/reactions" - } - } -} -``` - -The `reactions` field is the URI of the reaction `Collection`. - -The server **MUST** respond with a [Collection](../structures/collection) object that contains a list of `Reaction` objects, as such: - -```json5 -{ - "type": "Collection", - "first": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19/reactions?page=1", - "last": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19/reactions?page=1", - "total_count": 1, - // "author": ... (for signatures) - "items": [ - { - "type": "Extension", - "extension_type": "org.lysand:reactions/Reaction", - "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "content": "😀", - } - ] -} -``` - -### Public Reaction Federation - -If a user reacts to a [Note](../objects/note), the user's server **MUST** federate the reaction to all its followers. This is to ensure that all users see the reaction. - -Note, however, that this does not mean that the reaction will be displayed to users. If the [Note](../objects/note), that was reacted to is not visible to a user, the reaction **MUST NOT** be displayed to the user, even if the user follows the user that reacted to the [Note](../objects/note),. - -### Private Reaction Federation - -If a user reacts to a [Note](../objects/note),, the user's server **MUST** federate the reaction to the author of the [Note](../objects/note),. This is to ensure that the author of the [Note](../objects/note), sees the reaction. - -### Reactions With Custom Emojis - -If you implement both the Reactions and the Custom Emojis extensions, you can use the Custom Emojis extension to react with custom emojis. - -The Reaction object needs to be modified as such: - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:reactions/Reaction", - "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "content": ":happy_face:", - "extensions": { - "org.lysand:custom_emojis": { - "emojis": [ - { - "name": "happy_face", - "url": { - "image/webp": { - "content": "https://cdn.example.com/emojis/happy_face.webp", - } - } - }, - // ... - ] - } - } -} -``` - -The only addition to the Reaction object is the `extensions` field, which contains the [Custom Emojis](./custom-emojis) extension. - -When rendering the Reaction object, clients **MUST** replace the `:emoji_name:` with the appropriate emoji. If the client does not support custom emojis, it **MUST** display the `:emoji_name:` as-is. - -This emoji must be rendered according to the rules of the [Custom Emojis](./custom-emojis) extension. - -## Types - -```typescript -interface Reaction extends Extension { - extension_type: "org.lysand:reactions/Reaction"; - object: string; - content: string; -} -``` \ No newline at end of file diff --git a/docs/extensions/reports.md b/docs/extensions/reports.md deleted file mode 100644 index 5ca4823..0000000 --- a/docs/extensions/reports.md +++ /dev/null @@ -1,70 +0,0 @@ -# Reports - -The Reports extension enables users to flag content or users that violate the server's rules. This feature is important for maintaining a safe community environment. - -If the reporting user (reporter) and the reported user (reportee) are on the same server, the report can be handled directly without the need for federation. However, if the reporter and reportee are on different servers, the report **MUST** be federated to the reportee's server. - -## Report Object - -The report object encapsulates the details of the report. It is structured as follows: - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:reports/Report", - "author": "https://example.com/users/6f3001a1-641b-4763-a9c4-a089852eec84", - "id": "6f3001a1-641b-4763-a9c4-a089852eec84", - "uri": "https://example.com/actions/f7bbf7fc-88d2-47dd-b241-5d1f770a10f0", - "objects": [ - "https://test.com/publications/46f936a3-9a1e-4b02-8cde-0902a89769fa", - "https://test.com/publications/213d7c56-fb9b-4646-a4d2-7d70aa7d106a" - ], - "reason": "spam", - "comment": "This is spam." -} -``` - -Report events **MUST** be sent to the server actor's inbox. - -## Fields - -### Objects - -| Name | Type | Required | -| :------ | :-------------- | :------- | -| objects | Array of String | Yes | - -URIs of the objects that are being reported. - -If `objects` contains Actors, then these Actors **MUST** be treated as the reported users. - -If `objects` contains Notes, then these Notes **MUST** be treated as the reported content. - -`objects` can contain any URI to any kind of objects, however, typically only Actors or Notes should be reportable. - -### Reason - -| Name | Type | Required | -| :----- | :----- | :------- | -| reason | String | Yes | - -The reason for the report. This should be a concise summary of the report, such as `"spam"`, `"hate speech"`, `"tos violation"`, etc. - -### Comment - -| Name | Type | Required | -| :------ | :----- | :------- | -| comment | String | No | - -Additional comments about the report. This is meant to provide a more detailed description of the report, such as `"This user has been spamming my inbox with advertisements."`. - -## Types - -```typescript -interface Report extends Extension { - extension_type: "org.lysand:reports/Report"; - objects: string[]; - reason: string; - comment?: string; -} -``` \ No newline at end of file diff --git a/docs/extensions/server-endorsement.md b/docs/extensions/server-endorsement.md deleted file mode 100644 index a202b65..0000000 --- a/docs/extensions/server-endorsement.md +++ /dev/null @@ -1,78 +0,0 @@ -# Server Endorsement - -The Server Endorsement extension provides a mechanism for servers to vouch for the credibility of other servers. This is a valuable alternative to unrestricted federation, enabling servers to endorse those they deem trustworthy. Federation will only occur with endorsed servers and their subsequent endorsements, up to an admin-defined depth. - -## Endorsement Entity - -The endorsement entity encapsulates the details of an endorsement. It adheres to the following structure: - -```json5 -{ - "type": "Extension", - "extension_type": "org.lysand:server_endorsement/Endorsement", - "author": "https://example.com/actor", // The endorsing server's actor - "id": "ed480922-b095-4f09-9da5-c995be8f5960", - "uri": "https://example.com/actions/ed480922-b095-4f09-9da5-c995be8f5960", - "server": "https://example.com", -} -``` - -Endorsement entities **MUST** be dispatched to the endorsing server actor's inbox whenever the server administrators create a new endorsement. - -### Server - -| Name | Type | Required | -| :----- | :----- | :------- | -| server | String | Yes | - -URI of the endorsed server. This URI **MUST** correspond to the server's root endpoint, such as `https://example.com`. - -This URI **MUST NOT** be an IP address, except for development purposes. - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -The `author` field **MUST** represent the endorsing server actor. This requirement ensures that endorsements can be cryptographically signed using the server actor's `public_key`. - -## Endorsement Collection - -The URI for the endorsement collection can be specified within the server's metadata, under `extensions`: - -```json5 -{ - // ... - "extensions": { - "org.lysand:server_endorsement": { - "endorsements": "https://example.com/endorsements" - } - } - // ... -} -``` - -This should return a [Collection](../structures/collection) of `Endorsement` entities. - -## Endorsement Protocol Behavior - -Upon receiving an endorsement, a server **MUST** validate the endorsement's signature. If the signature is invalid, the server **MUST** disregard the endorsement. This also applies when fetching the endorsement collection. - -The server has the discretion to decide how to handle the endorsement. Endorsements are intended to serve as a dynamic whitelist for servers, with administrators initially selecting a few trusted servers and progressively adding more. - -Servers **SHOULD** display received endorsements to their administrators, allowing them to accept or reject the endorsement. Ultimately, the decision on how to handle the endorsement lies with the administrators. - -Endorsements should be viewed as a seal of trust: If a server endorses another, it is expressing its trust in that server. Servers should refrain from endorsing servers they do not trust. - -Lastly, servers **MAY** verify the endorsements of the servers they have endorsed, up to a user-defined depth. This creates a trust chain, where servers endorse those they trust, and those servers, in turn, endorse servers they trust, and so forth. - -## Types - -```typescript -interface Endorsement extends Extension { - extension_type: "org.lysand:server_endorsement/Endorsement"; - author: string; - server: string; -} -``` \ No newline at end of file diff --git a/docs/extensions/vanity.md b/docs/extensions/vanity.md deleted file mode 100644 index 3b20094..0000000 --- a/docs/extensions/vanity.md +++ /dev/null @@ -1,184 +0,0 @@ -# Vanity - -The Vanity extension allows users to customize their profile in a more in-depth manner. - -Here is an example object: -```json5 -{ - // ... - "type": "User", - // ... - "extensions": { - "org.lysand:vanity": { - "avatar_overlay": { - "image/png": { - "content": "https://cdn.example.com/ab5081cf-b11f-408f-92c2-7c246f290593/cat_ears.png", - "content_type": "image/png" - } - }, - "avatar_mask": { - "image/png": { - "content": "https://cdn.example.com/d8c42be1-d0f7-43ef-b4ab-5f614e1beba4/rounded_square.jpeg", - "content_type": "image/jpeg" - } - }, - "background": { - "image/png": { - "content": "https://cdn.example.com/6492ddcd-311e-4921-9567-41b497762b09/untitled-file-0019822.png", - "content_type": "image/png" - } - }, - "audio": { - "audio/mpeg": { - "content": "https://cdn.example.com/4da2f0d4-4728-4819-83e4-d614e4c5bebc/michael-jackson-thriller.mp3", - "content_type": "audio/mpeg" - } - }, - "pronouns": { - "en-us": [ - "he/him", - { - "subject": "they", - "object": "them", - "dependent_possessive": "their", - "independent_possessive": "theirs", - "reflexive": "themself" - }, - ] - }, - "birthday": "1998-04-12", - "location": "+40.6894-074.0447/", - "activitypub": [ - "@erikuden@mastodon.de" - ], - "aliases": [ - "https://burger.social/accounts/349ee237-c672-41c1-aadc-677e185f795a", - "https://social.lysand.org/users/f565ef02-035d-4974-ba5e-f62a8558331d" - ] - } - } -} -``` - -The extension name is `org.lysand:vanity`. All properties are optional. - -## Fields - -### Avatar Overlay - -| Name | Type | Required | -| :------------- | :------------ | :------- | -| avatar_overlay | ContentFormat | No | - -An overlay to be placed on top of the user's avatar. This can be used to add accessories, such as hats or glasses. Overlay should always be a transparent image. - -### Avatar Mask - -| Name | Type | Required | -| :---------- | :------------ | :------- | -| avatar_mask | ContentFormat | No | - -A mask to be applied to the user's avatar. This can be used to change the shape of the avatar, such as making it a circle or a rounded square. Mask should be a fully black (#000000) image with the shape of the mask being transparent. As such, a rounded square mask should have a fully black square with rounded corners. - -### Background - -| Name | Type | Required | -| :--------- | :------------ | :------- | -| background | ContentFormat | No | - -A background image to be displayed on the user's profile. This should be a full-width high-resolution image, preferably at least 1920x1080 pixels. - -Space-efficient formats such as WebP are recommended. - -### Audio - -| Name | Type | Required | -| :---- | :------------ | :------- | -| audio | ContentFormat | No | - -An audio file to be played on the user's profile. This can be used to add a theme song or a voice introduction. - -> [!WARNING] -> Audio files should be muted by default and should not autoplay. Users should have the option to play the audio file, or disable them entirely. -> -> Furthermore, audio file support in this extension should be toggleable per server, as it can be a potential vector for abuse. - -### Pronouns - -| Name | Type | Required | -| :------- | :----------------------------------- | :------- | -| pronouns | Array of ShortPronoun or LongPronoun | No | - -An array of pronouns the user uses. Pronouns can be represented as a string or an object. - -See [Types](#types) for a full description of the `ShortPronoun` and `LongPronoun` types. - -### Birthday - -| Name | Type | Required | -| :------- | :----- | :------- | -| birthday | String | No | - -The user's birthday. This should be in the format `YYYY-MM-DD` (ISO 8601). If the year is set to `0000`, clients should not display the year. - -Clients might choose to display the user's age as well when the year is present. - -### Location - -| Name | Type | Required | -| :------- | :----- | :------- | -| location | String | No | - -The user's location. This should be a string of GPS data as defined in [ISO 6709 Annex H](https://en.wikipedia.org/wiki/ISO_6709#String_expression_(Annex_H)), or alternatively a raw string such as "New York, NY". GPS data does not need to be precise, and can be as simple as `+46+002/` (France) or `+48.52+002.20/` (Paris, France). - -Clients might choose to display a map of the user's location. - -### ActivityPub - -| Name | Type | Required | -| :--------- | :----- | :------- | -| activitypub | Array of String | No | - -The user's ActivityPub profile. This should be an array of strings in the format `@username@domain`. - -Servers are expected to use standard WebFinger resolution to fetch the user's ActivityPub profile if needed. - -### Aliases - -| Name | Type | Required | -| :------ | :----- | :------- | -| aliases | Array of String | No | - -Aliases to the user's profile on other Lysand-compatible servers. This should be an array of URIs resolving to the user's Lysand object. - -## Types - -```typescript -interface VanityExtension { - avatar_overlay?: ContentFormat; - avatar_mask?: ContentFormat; - background?: ContentFormat; - audio?: ContentFormat; - pronouns?: { - [language: string]: (ShortPronoun | LongPronoun)[]; - }; - birthday?: string; - location?: string; - activitypub?: string[]; - aliases?: string[]; -} -``` - -```typescript -type ShortPronoun = string; -``` - -```typescript -interface LongPronoun { - subject: string; - object: string; - dependent_possessive: string; - independent_possessive: string; - reflexive: string; -} -``` \ No newline at end of file diff --git a/docs/federation/endpoints.md b/docs/federation/endpoints.md deleted file mode 100644 index f0ac925..0000000 --- a/docs/federation/endpoints.md +++ /dev/null @@ -1,198 +0,0 @@ -# Federation - -This section explains how federation operates within Lysand. - -Lysand's federation is built upon the HTTP stack. Servers interact with each other by exchanging HTTP requests. - -These requests are predominantly `POST` requests that carry a JSON object in the body. This JSON object **MUST** conform to the Lysand object schema. - -Servers that receive non-conforming Lysand objects **SHOULD** disregard these objects as invalid, and return a `400 Bad Request` response code (these could include debugging information in the response body). - -> [!NOTE] -> Values such as `https://example.com/users/uuid` are example implementations and not a guide on how an implementation should format a URI. These must follow the rules outlined in [the base spec](../spec.md). -> -## User Actor Endpoints - -When a server aims to retrieve the profile of a user on a differentserver, it follows the process outlined in [User Discovery](user-discovery). - -Upon discovering the target server's endpoints, the requesting server can issue a `GET` request to the user's endpoint to retrieve the user's actor. - -For instance, to fetch user information, the requesting server **MUST** send a `GET` request to the endpoint `https://example.com/users/uuid` with the required headers. The server can use either the [Server Actor](/federation/server-actor) or a requesting user's actor to sign the request, as appropriate depending on which actor the request is associated with. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Actor](../objects/actors) object. - -> [!NOTE] -> While servers are not obligated to implement functionality between certain endpoints such as dislikes or featured notes, they **MUST** at least return an empty collection for these endpoints. Servers may also disregard any objects they do not handle, but should return a success response code. - -## User Inbox - -After the requesting server has discovered the target server's endpoints, it can issue a `POST` request to the `inbox` endpoint to transmit an object to the user. This process mirrors how objects are sent in ActivityPub. - -Typically, the inbox can be found at the same URL as the user's actor, but this is not a requirement. The server **MUST** specify the inbox URL in the actor object. - -Example inbox URL: `https://example.com/users/uuid/inbox` - -The requesting server **MUST** send a `POST` request to the endpoint `https://example.com/users/uuid/inbox` with the headers `Content-Type: application/json` and `Accept: application/json`. - -The body of the request **MUST** contain a valid Lysand object. - -Example with cURL (without signature): -```bash -curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{ \ - "type":"Publication", \ - "id":"6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", \ - "uri":"https://example.com/publications/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", \ - "author":"https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", \ - "created_at":"2021-01-01T00:00:00.000Z", \ - "contents":"Hello, world!" \ -}' https://example.com/users/uuid/inbox -``` - -The server **MUST** respond with a `201 CREATED` response code if the operation was successful. - -## User Outbox - -In Lysand, users have an outbox, which is a list of objects that the user has posted. This is akin to the outbox in ActivityPub. - -The server **MUST** specify the outbox URL in the actor object. - -Example outbox URL: `https://example.com/users/uuid/outbox` - -The requesting server **MUST** send a `GET` request to the outbox endpoint (`https://example.com/users/uuid/outbox`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). - -Example: - -```json5 -{ - "first": "https://example.com/users/uuid/outbox?page=1", - "last": "https://example.com/users/uuid/outbox?page=1", - // No next or prev attribute in this case, but they can exist - "total_items": 1, - "items": [ - { - "type": "Note", - "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "uri": "https://example.com/publications/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "author": "https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755", - "created_at": "2021-01-01T00:00:00.000Z", - "contents": [ - { - "content": "Hello, world!", - "content_type": "text/plain" - } - ], - } - ] -} -``` - -These publications **MUST BE** ordered from newest to oldest, in descending order. - -## User Followers - -Users in Lysand have a list of followers, which is a list of users that follow the user. This is similar to the followers list in ActivityPub. - -> [!NOTE] -> If you prefer not to display this list publicly, you can configure the followers endpoint to return an empty collection. - -The server **MUST** specify the followers URL in the actor object. - -Example followers URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/followers` - -The requesting server **MUST** send a `GET` request to the followers endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/followers`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Actors](../objects/actors). This collection may be empty. - -## User Following - -Users in Lysand have a followlist, which is a list of users that the user follows. This is similar to the following list in ActivityPub. - -> [!NOTE] -> If you prefer not to display this list publicly, you can configure the following endpoint to return an empty collection. - -The server **MUST** specify the following URL in the actor object. - -Example following URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/following` - -The requesting server **MUST** send a `GET` request to the following endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/following`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Actors](../objects/actors). This collection may be empty. - -## User Featured Publications - -Users in Lysand have a list of featured publications, which is a list of publications that the user has pinned or deemed important. This is similar to the featured publications list in ActivityPub. - -> [!NOTE] -> If you prefer not to display this list publicly, you can configure the featured publications endpoint to return an empty collection. - -The server **MUST** specify the featured publications URL in the actor object. - -Example featured publications URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/featured` - -The requesting server **MUST** send a `GET` request to the featured publications endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/featured`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty. - -## User Likes - -Users in Lysand have a list of likes, which is a list of posts that the user has liked. This is similar to the likes list in ActivityPub. - -> [!NOTE] -> If you prefer not to display this list publicly, you can configure the likes endpoint to return an empty collection. - -The server **MUST** specify the likes URL in the actor object. - -Example likes URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/likes` - -The requesting server **MUST** send a `GET` request to the likes endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/likes`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty. - - -## User Dislikes - -Users in Lysand have a list of dislikes, which is a list of posts that the user has disliked. This is similar to the dislikes list in ActivityPub. - -> [!NOTE] -> If you prefer not to display this list publicly, you can configure the dislikes endpoint to return an empty collection. - -The server **MUST** specify the dislikes URL in the actor object. - -Example dislikes URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/dislikes` - -The requesting server **MUST** send a `GET` request to the dislikes endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/dislikes`) with the headers `Accept: application/json`. - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty. - -## Server Discovery - -> [!NOTE] -> The terms "the server" and "the requesting server" are used in this section. "The server" refers to the server that is being discovered, and "the requesting server" refers to the server that is attempting to discover the server. - -To retrieve the metadata of a server, the requesting server **MUST** send a `GET` request to the endpoint `https://example.com/.well-known/lysand`. - -The requesting server **MUST** send the following headers with the request: -``` -Accept: application/json -``` - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [ServerMetadata](../objects/server-metadata) object. - -Example: - -```json5 -{ - "type": "ServerMetadata", - "name": "Example", - "uri": "https://example.com", - "version": "1.0.0", - "supported_extensions": [ - "org.lysand:reactions", - "org.lysand:polls", - "org.lysand:custom_emojis", - "org.lysand:is_cat" - ] -} -``` diff --git a/docs/federation/server-actor.md b/docs/federation/server-actor.md deleted file mode 100644 index 7bf937c..0000000 --- a/docs/federation/server-actor.md +++ /dev/null @@ -1,11 +0,0 @@ -# Server Actor - -Servers **MUST** have an Actor object that represents the server. This Actor object **MUST** be a valid User object. - -The Actor object can be found by sending a WebFinger request to the server's WebFinger endpoint for `actor@server.com`. For more information about WebFinger, please see [User Discovery](/federation/user-discovery). - -The Actor object **MUST** contain a `public_key` field that contains the public key of the server. This public key **MUST** be used to sign all requests sent by the server when the `author` field of an object is the server actor. - -The server actor **MUST** be used to sign all requests sent by the server when the `author` field of an object is the server actor. - -The server actor **SHOULD** contain empty data fields, such as `display_name` and `bio`. However, if the server actor does contain data fields, they **MUST** be valid, as with any actor. \ No newline at end of file diff --git a/docs/federation/user-discovery.md b/docs/federation/user-discovery.md deleted file mode 100644 index 96ce849..0000000 --- a/docs/federation/user-discovery.md +++ /dev/null @@ -1,78 +0,0 @@ -# User Discovery - -> [!NOTE] -> The terms "the server" and "the requesting server" are used in this section. "The server" refers to the server that is being discovered, and "the requesting server" refers to the server that is trying to discover the server. - -Servers **MUST** implement the [WebFinger](https://tools.ietf.org/html/rfc7033) protocol to allow other servers to discover their endpoints. This is done by serving a `host-meta` file at the address `/.well-known/host-meta`. - -The document **MUST** contain the following information, as specified by the WebFinger protocol: - -```xml - - - - -``` - -The `template` field **MUST** be the URI of the server's WebFinger endpoint, which is usually `https://example.com/.well-known/webfinger?resource={uri}`. - -The `resource` field **MUST** be the URI of the user that the server is trying to discover (in the format `acct:identifier@example.com`) - -Breaking down this URI, we get the following: - -- `acct`: The protocol of the URI. This is always `acct` for Lysand. -- `identifier`: Either the UUID or the username of the user that the server is trying to discover. -- `example.com`: The domain of the server that the user is on. This is usually the domain of the server. This can also be a subdomain of the server, such as `lysand.example.com`. - -This format is reminiscent of the `acct` format used by ActivityPub, but with either a UUID or a username instead of just an username. Users will typically not use the `id` of an actor to identify it, but instead its `username`: servers **MUST** only use the `id` to identify actors. - ---- - -Once the server's WebFinger endpoint has been discovered, it can receive a `GET` request to the endpoint to discover the endpoints of the user. - -The requesting server **MUST** send a `GET` request to the endpoint `https://example.com/.well-known/webfinger`. - -The requesting server **MUST** send the following headers with the request: - -- `Accept: application/jrd+json` -- `Accept: application/json` - -The requestinng server **MUST** send the following query parameters with the request: - -- `resource`: The URI of the user that the server is trying to discover (in the format `acct:identifier@example.com` (replace `identifier` with the user's ID or username) - ---- - -The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** contain the following information, as specified by the WebFinger protocol: - -```json5 -{ - "subject": "acct:identifier@example.com", - "links": [ - { - "rel": "self", - "type": "application/json", - "href": "https://example.com/users/uuid" - }, - // The following links are optional but could be added to server software to display XML feeds and an HTML profile page - { - "rel": "http://webfinger.net/rel/profile-page", - "type": "text/html", - "href": "https://example.com/users/uuid" - }, - { - "rel": "http://schemas.google.com/g/2010#updates-from", - "type": "application/atom+xml", - "href": "https://example.com/users/uuid" - }, - ] -} -``` - -> [!NOTE] -> The `subject` field **MUST** be the same as the `resource` field in the request. - -> [!NOTE] -> The server implementation is free to add any additional links to the `links` array, such as for compatibility with other federation protocols. However, the links specified above **MUST** be included. -> -> The `href` values of these links can be anything as long as it includes the `uuid` of the user, such as `https://example.com/accounts/uuid` or `https://example.com/uuid.`. diff --git a/docs/groups.md b/docs/groups.md deleted file mode 100644 index 55c1c76..0000000 --- a/docs/groups.md +++ /dev/null @@ -1,67 +0,0 @@ -# Groups - -Groups are a way to organize the visibility of objects on the server. Groups can be thought of as something similar to a Matrix room or a Discord channel, while also being similar to a Mastodon list. - -> [!NOTE] -> Groups replace the old "visibility" system for Notes, which was designed for a microblogging context. Groups are more flexible and can be used for any application. -> -> Notes can still use visibility in cases where groups are not needed with the `followers` and `public` values where you'd typically put a group URI (for example, in a [Publication](./objects/publications.md)'s `group` field'). - -# Group Entity - -The group entity encapsulates the details of a group. It adheres to the following structure: - -```json5 -{ - "type": "Group", - "id": "ed480922-b095-4f09-9da5-c995be8f5960", - "uri": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960", - "name": { - "text/html": { - "content": "The Woozy fan club" - } - }, - "description": { - "text/plain": { - "content": "A group for fans of the Woozy emoji." - } - }, - "members": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960/members", -} -``` - -## Fields - -### Name - -| Name | Type | Required | -| :--- | :------------ | :------- | -| name | ContentFormat | No | - -The name of the group. This field is optional. Can contain custom emojis, like most other text fields. - -### Description - -| Name | Type | Required | -| :---------- | :------------ | :------- | -| description | ContentFormat | No | - -A description of the group. This field is optional. Can contain custom emojis, like most other text fields. - -### Members - -| Name | Type | Required | -| :------ | :----- | :------- | -| members | String | Yes | - -The URI of the group's members list. This field is required. Resolves to a [Collection](./structures/collection) of [User](./objects/user) objects. - -## Implementation - -`Note` objects can be posted to groups by setting the `group` field to the URI of the group. If there is no `group` field, the note is posted to whoever is mentioned in the `to` field. - -Other values for `group` are: -- `public` for public notes, which can be seen by anyone. -- `followers` for notes that can be seen by the author's followers only. - -If the `group` field is empty, and nobody is mentioned in the `to` field, the note is only visible to the author. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index ef819a6..0000000 --- a/docs/index.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "Lysand" - text: "Federation, simpler" - tagline: A simple to implement and complete federation protocol - image: - src: https://cdn.lysand.org/logo.webp - alt: Lysand Logo - actions: - - theme: brand - text: Protocol Docs - link: /spec - - theme: alt - text: Lysand Server - link: https://github.com/lysand-org/lysand ---- - - - - - - \ No newline at end of file diff --git a/docs/objects.md b/docs/objects.md deleted file mode 100644 index 2d1db00..0000000 --- a/docs/objects.md +++ /dev/null @@ -1,62 +0,0 @@ -# Data Entities - -Lysand employs JSON (JavaScript Object Notation) entities for its data structure. This format is designed to be straightforward, facilitating easy implementation and comprehension. - -All JSON entities such as [Publications](objects/publications), [Actors](objects/actors), and [Actions](objects/actions) **MUST** include the following attributes: - -## Identifier (ID) - -The `id` attribute of an Entity is a string that serves as the unique identifier of the entity. It is utilized to distinguish the entity, and **MUST** be unique among all entities on the same server. - -While the `id` attribute is not mandated to be unique across the entire network, it is advisable to do so. Servers **MUST** employ UUIDs or a UUID-compatible system for the `id` attribute. - -## Creation Timestamp - -The `created_at` attribute of an entity is a string that signifies the date and time when the entity was created. It is used to sequence the entities. The data **MUST** adhere to the ISO 8601 format. - -Example: `2021-01-01T00:00:00.000Z` - -> [!NOTE] -> The `created_at` attribute should reflect the actual date and time of the post, but it is not mandatory. Any ISO 8601 date is permissible in the `created_at` field. Servers have the discretion to process dates they deem invalid, such as future dates. - -## Uniform Resource Identifier (URI) - -The `uri` attribute of an entity is a string that signifies the URI of the entity. It is used to identify the entity, and **MUST** be unique among all entities. This URI **MUST** be unique across the entire network, and include the `id` of the entity in the URI. - -URIs must adhere to the rules defined [here](spec). - -## Entity Type - -The `type` attribute of an entity is a string that signifies the type of the entity. It is used to determine how the entity should be presented to the user. - -The `type` attribute **MUST** a type officially defined in the Lysand protocol. Extension types are **NOT** permitted and should instead use the [Extension System](extensions.md). - -# Types - -This document uses TypeScript to define the types of the entities in a clear and universal manner. TypeScript is a superset of JavaScript that adds static type definitions to the language. The types are defined in the following format: - -```typescript -interface Entity { - id: string; - created_at: string; - uri: string; - type: string; - extensions?: { - "org.lysand:custom_emojis"?: { - emojis: Emoji[]; - }; - [key: string]: object | undefined; - }; -} -``` - -The `Entity` type is the base type for all entities in the Lysand protocol. It includes the `id`, `created_at`, `uri`, and `type` attributes. - -Other entities described in other parts of this documentation will extend the `Entity` type to include additional attributes, such as: - -```typescript -interface ImaginaryNote extends Entity { - content: string; - mentions: string[]; -}; -``` \ No newline at end of file diff --git a/docs/objects/actions.md b/docs/objects/actions.md deleted file mode 100644 index 733a72d..0000000 --- a/docs/objects/actions.md +++ /dev/null @@ -1,52 +0,0 @@ -# User Interactions - -User interactions in the Lysand protocol are primarily facilitated through Actions. These are JSON objects that encapsulate a user's intended operation, such as favouriting an object or initiating a follow request to another user. - -Actions are a broad category encompassing various types of objects, rather than being a specific object type. - -Here's an example of an Action: - -```json5 -{ - "type": "Like", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19" -} -``` - -## Action Types - -The currently supported action types include: -- [`Like`](./like) -- [`Dislike`](./dislike) -- [`Follow`](./follow) -- [`FollowAccept`](./follow-accept) -- [`FollowReject`](./follow-reject) -- [`Announce`](./announce) -- [`Undo`](./undo) - -Notably, the Lysand protocol does not include a `Block` action. This is because Lysand does not inherently support the concept of blocking users. Instead, the decision to display or hide content from a particular user should be done server-side. - -This approach helps prevent potential misuse of the protocol to determine if a user has been blocked by another, thereby addressing a significant privacy concern. - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who initiated the action. - -## Types - -```typescript -interface Action extends Entity { - type: "Like" | "Dislike" | "Follow" | "FollowAccept" | "FollowReject" | "Announce" | "Undo"; - author: string -} -``` \ No newline at end of file diff --git a/docs/objects/actors.md b/docs/objects/actors.md deleted file mode 100644 index 7f31639..0000000 --- a/docs/objects/actors.md +++ /dev/null @@ -1,48 +0,0 @@ -# Actors - -Actors are the primary users of the Lysand protocol. They are JSON objects that symbolize a user, akin to ActivityPub's `Actor` objects. - -Actors encompass two distinct object types currently, namely [Users](./user) and [Server Actors](../federation/server-actor). - -Here is a sample Actor: - -```json5 -{ - "type": "User", - "id": "02e1e3b2-cb1f-4e4a-b82e-98866bee5de7", - "uri": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7", - "created_at": "2021-01-01T00:00:00.000Z", - "display_name": "Gordon Ramsay", - "username": "gordonramsay", - "avatar": { - "image/png": { - "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png", - }, - }, - "header": { - "image/png": { - "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png", - }, - }, - "indexable": true, - "public_key": { - "public_key": "...", - "actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" - }, - "bio": { - "text/plain": { - "content": "Hello!", - }, - }, - "fields": [], - "featured": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/featured", - "followers": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/followers", - "following": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/following", - "likes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/likes", - "dislikes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/dislikes", - "inbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/inbox", - "outbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/outbox", -} -``` - -For detailed information on their structure, please refer to the respective documentation for [User](./user) and [Server Actor](../federation/server-actor). \ No newline at end of file diff --git a/docs/objects/announce.md b/docs/objects/announce.md deleted file mode 100644 index ecb8901..0000000 --- a/docs/objects/announce.md +++ /dev/null @@ -1 +0,0 @@ -This page has been moved to the [Microblogging Extension](../extensions/microblogging#announce). diff --git a/docs/objects/dislike.md b/docs/objects/dislike.md deleted file mode 100644 index 8b7002c..0000000 --- a/docs/objects/dislike.md +++ /dev/null @@ -1,42 +0,0 @@ -# Dislike - -The Dislike action signifies a user's negative sentiment towards an object. It is a frequently used action in the Lysand protocol. - -Example: -```json5 -{ - "type": "Dislike", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who initiated the action. - -### Object - -| Name | Type | Required | -| :----- | :----- | :------- | -| object | String | Yes | - -URI of the object being disliked. Must be of type [Note](./note) - -## Types - -```typescript -interface Dislike extends Action { - type: "Dislike"; - object: string; -} -``` \ No newline at end of file diff --git a/docs/objects/follow-accept.md b/docs/objects/follow-accept.md deleted file mode 100644 index d595c28..0000000 --- a/docs/objects/follow-accept.md +++ /dev/null @@ -1,42 +0,0 @@ -# Follow Accept - -A FollowAccept action symbolizes a user's consent to a follow request from another user. Upon acceptance, the user will start receiving the other user's posts in their feed. - -Example: -```json5 -{ - "type": "FollowAccept", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who was being follow requested - -### Follower - -| Name | Type | Required | -| :----- | :----- | :------- | -| follower | String | Yes | - -URI of the [User](./user) who tried to follow the author - -## Types - -```typescript -interface FollowAccept extends Action { - type: "FollowAccept"; - follower: string; -} -``` \ No newline at end of file diff --git a/docs/objects/follow-reject.md b/docs/objects/follow-reject.md deleted file mode 100644 index aed6449..0000000 --- a/docs/objects/follow-reject.md +++ /dev/null @@ -1,42 +0,0 @@ -# Follow Reject - -A FollowReject action signifies a user's decision to decline a follow request from another user. This rejection prevents the requesting user's posts from appearing in the recipient's feed. - -Example: -```json5 -{ - "type": "FollowReject", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who was being follow requested. - -### Follower - -| Name | Type | Required | -| :----- | :----- | :------- | -| follower | String | Yes | - -URI of the [User](./user) who tried to follow the author. - -## Types - -```typescript -interface FollowReject extends Action { - type: "FollowReject"; - follower: string; -} -``` diff --git a/docs/objects/follow.md b/docs/objects/follow.md deleted file mode 100644 index 7d907bd..0000000 --- a/docs/objects/follow.md +++ /dev/null @@ -1,42 +0,0 @@ -# Follow - -A Follow action embodies a user's intent to follow another user. Upon successful execution of this action, the follower will start receiving the followed user's posts in their feed. This action facilitates the creation of a personalized content stream for each user. - -Example: -```json5 -{ - "type": "Follow", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "followee": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who initiated the action. - -### Followee - -| Name | Type | Required | -| :------- | :----- | :------- | -| followee | String | Yes | - -URI of the [User](./user) who is being follow requested. - -## Types - -```typescript -interface Follow extends Action { - type: "Follow"; - followee: string; -} -``` diff --git a/docs/objects/like.md b/docs/objects/like.md deleted file mode 100644 index b80ebba..0000000 --- a/docs/objects/like.md +++ /dev/null @@ -1,42 +0,0 @@ -# Like - -The Dislike action signifies a user's positive sentiment towards an object, akin to a favourite. It is a frequently used action in the Lysand protocol. - -Example: -```json5 -{ - "type": "Like", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who initiated the action. - -### Object - -| Name | Type | Required | -| :----- | :----- | :------- | -| object | String | Yes | - -URI of the object being liked. Must be of type [Note](./note) - -## Types - -```typescript -interface Like extends Action { - type: "Like"; - object: string; -} -``` diff --git a/docs/objects/note.md b/docs/objects/note.md deleted file mode 100644 index fb71073..0000000 --- a/docs/objects/note.md +++ /dev/null @@ -1,13 +0,0 @@ -# Note - -A `Note` object symbolizes a basic post or publication within the Lysand protocol. It is the most frequently used object type. - -`Note` objects extend all properties from the [Publication](./publications) object, thereby inheriting its characteristics and behaviors. - -### Types - -```typescript -interface Note extends Publication { - type: "Note"; -} -``` \ No newline at end of file diff --git a/docs/objects/patch.md b/docs/objects/patch.md deleted file mode 100644 index cc93488..0000000 --- a/docs/objects/patch.md +++ /dev/null @@ -1,65 +0,0 @@ -# Patch - -A `Patch` object represents a modification to a [Note](./note). It is primarily used to update a [Note](./note), for instance, to correct a typographical error. - -`Patch` objects are intended for internal server use and are not designed to be displayed to the user. - -Each subsequent patch is applied to the original object, not the preceding patch. The server is responsible for presenting the most recent patch stored to the client. - -> [!NOTE] -> A `Patch` object should replace the object it is patching when displayed to the client. Therefore, if a Patch object lacks some fields from the previous object, these fields should be removed in the edit. - -Here is a sample `Patch` for the aforementioned object: - -```json5 -{ - "type": "Patch", - "id": "4c21fdea-1318-4d14-b3aa-1ac2f3db2e53", - "uri": "https://example.com/publications/4c21fdea-1318-4d14-b3aa-1ac2f3db2e53", - "patched_id": "f08a124e-fe90-439e-8be4-15a428a72a19", - "patched_at": "2021-01-01T00:00:00.000Z", - "contents": [ - { - "content": "This is patched!", - "content_type": "text/plain" - }, - ], - // ... -} -``` - -## Fields - -### ID - -This ID must be distinct from the original Note object, but it does not replace the original Note object's ID. It serves to identify the Patch object. - -### Patched ID - -| Name | Type | Required | -| :--------- | :----- | :------- | -| patched_id | String | Yes | - -This is the URI of the object being patched. It must be a [Note](./note). - -### Patched At - -| Name | Type | Required | -| :--------- | :----- | :------- | -| patched_at | String | Yes | - -This is the date and time when the object was patched. It must be in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. - -### Other - -`Patch` objects inherit all other properties from [Publications](./publications). - -## Types - -```typescript -interface Patch extends Publication { - type: "Patch"; - patched_id: string; - patched_at: string; -} -``` diff --git a/docs/objects/publications.md b/docs/objects/publications.md deleted file mode 100644 index 235608c..0000000 --- a/docs/objects/publications.md +++ /dev/null @@ -1,346 +0,0 @@ -# Publications - -Publications are the main building blocks of the Lysand protocol. They are JSON objects that represent a publication, such as a post or a comment. - -Here is an example publication: -```json5 -{ - "type": "Note", - "id": "f08a124e-fe90-439e-8be4-15a428a72a19", - "uri": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "created_at": "2021-01-01T00:00:00.000Z", - "content": { - "text/plain": { - "content": "Hello, world! I own this website: https://google.com" - }, - "text/html": { - "content": "Hello, world! I own this website! https://google.com" - } - }, - "category": "microblog", - "device": { - "name": "Megalodon for Android", - "version": "1.3.89", - "url": "https://sk22.github.io/megalodon" - }, - "previews": [ - { - "link": "https://google.com", - "title": "Google", - "description": "The world's most popular search engine", - "image": "https://cdn.example.com/previews/6e0204a2-746c-4972-8602-c4f37fc63bbe.png", - "icon": "https://google.com/favicon.ico" - } - ], - "group": "public", - "attachments": [ - { - "image/png": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png" - }, - "image/webp": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp" - } - } - ], - "replies_to": "https://test.com/publications/0b6ecf49-2959-4590-afb6-232f57036fa6", - "mentions": [ - "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" - ], -} -``` - -## Fields - -### Type - -Currently available types are: -- [`Note`](./note) -- [`Patch`](./patch) - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the user that created the object. - -### Content - -| Name | Type | Required | -| :------ | :------------ | :------- | -| content | ContentFormat | No | - -Content of the Publication, such as note text for [Notes](./note). If it is not provided, it is assumed that the publication does not have any content. - -It is recommended that servers limit the length of the content from 500 to a couple thousand characters, but it is up to the server to decide how long the content can be. The protocol does not have an upper limit for the length of the content. - -The `content` field **MUST** be a text format, such as `text/plain` or `text/html`. The `content` field **MUST NOT** be a binary format, such as `image/png` or `video/mp4`. Platforms such as video sharing sites should use the `attachments` field for media instead. - -An example value for the `content` field would be: -```json5 -{ - // ... - "content": { - "text/plain": { - "content": "Hello, world!" - }, - "text/html": { - "content": "Hello, world!" - } - } - // ... -} -``` - -> [!NOTE] -> Lysand heavily recommends that servers support the `text/html` content type, as it is the richest content type that is supported by most clients. -> -> Lysand also recommends that servers always include a `text/plain` version of each object, as it is the most basic content type that is supported by all clients, such as command line clients. - -> [!WARNING] -> Servers should not trust the `text/html` content type, as it could contain malicious code. Servers should always sanitize the content before displaying it to the user. -> -> Additionally, frontends should warn users before clicking on links that do not match the link text, such as `https://google.com` - -It is up to the client to choose which content format to display to the user. The client may choose to display the first content format that it supports, or it may choose to display the content format that it thinks is the most appropriate. - -Clients should display the richest content format that they support, such as HTML or more exotic formats such as MFM. - -### Category - -| Name | Type | Required | -| :------- | :----------- | :------- | -| category | CategoryType | No | - -Category of the publication. Used for clients to possibly display notes in different ways, for example a note with the `microblog` category could be displayed in a timeline, while a note with the `forum` category could be displayed Reddit-style. - -See [the Types section](#types) for more information on the `CategoryType` enum. - -### Device - -| Name | Type | Required | -| :----- | :----- | :------- | -| device | Device | No | - -Device that the publication was created on. If it is not provided, it is assumed that the publication was created on a generic device. - -Servers should avoid collecting any information that could be used to identify the user, such as IP addresses or user agents. A simple name is recommended. - -### Previews - -| Name | Type | Required | -| :------- | :------------------- | :------- | -| previews | Array of LinkPreview | No | - -Previews for links in the publication. Optional. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every server that sees the publication, creating an accidental DDOS attack. - -> [!WARNING] -> Servers should make sure not to trust the previews, as they could be faked by remote servers. This is not a very good attack vector, but it is still possible to redirect users to malicious links. - -### Group - -| Name | Type | Required | -| :---- | :----- | :------- | -| group | String | No | - -URI of a [Group](../groups.md), or `public` or `followers`. - -Refer to the [Groups](../groups.md) page for more information on groups, their implementation and what to do if this value is not provided. - -### Attachments - -| Name | Type | Required | -| :---------- | :--------------------- | :------- | -| attachments | Array of ContentFormat | No | - -Contains list of attachments for the publication in [ContentFormat](../structures/content-format) structure. f it is not provided, it is assumed that the publication does not have any attachments. - -It is recommended that servers limit the number of attachments to 20, but it is up to the server to decide how many attachments a publication can have. The protocol does not have an upper limit for the number of attachments. - -The `attachments` field **MAY** be in any format, such as `image/png`, `image/webp`, `video/mp4`, or `audio/mpeg`. It is up to the server to decide which formats are allowed. - -> [!NOTE] -> Lysand recommends that servers let users upload any file as an attachment, but clients should warn users before downloading potentially malicious files, such as `.exe` files. - -### Replies To - -| Name | Type | Required | -| :--------- | :----- | :------- | -| replies_to | String | No | - -URI of the `Note` that the publication is replying to, if any. Used to determine the reply chain of an object. - -### Quotes - -| Name | Type | Required | -| :----- | :----- | :------- | -| quotes | String | No | - -URI of the `Note` that the publication is quoting, if any. It is used to determine the quote chain of an object. - -Quoting is similar to replying, but it does not (by default) notify the user that they were quoted. It is meant to be used to comment or add context to another publication. - -Example of quoting: -```json5 -{ - // ... - "quotes": "https://test.com/publications/5f886c84-f8f7-4f65-8ac2-4691d385c509", - // ... -} -``` - -Quoting **SHOULD BE** rendered differently from replying, such as by adding a quote block to the publication or including the quoted post in the publication. - -### Mentions - -| Name | Type | Required | -| :------- | :-------------- | :------- | -| mentions | Array of String | No | - -URIs of users that the publication is mentioning. (such as `@username@server.social` in a note). If it is not provided, it is assumed that the publication is not mentioning any other users. - -An example value for `mentions` would be: -```json5 -{ - // ... - "mentions": [ - "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" - ] - // ... -} -``` - -### Subject - -| Name | Type | Required | -| :------ | :----- | :------- | -| subject | String | No | - -Subject of the publication. May be used as a content warning or spoiler warning. If not provided or is empty, there is no subject. - -It is recommended that servers limit the length of the subject from 1 to 300 characters, but it is up to the server to decide how long the subject can be. The protocol does not have an upper limit for the length of the subject. - -The `subject` field **MUST NOT** be a `ContentFormat` object. It **MUST** be a string, and **MUST** be plain text. It **MUST NOT** contain any HTML or other markup. - -See [ContentFormat](/structures/content-format) for more information on `ContentFormat` objects. - -Client extensions are welcome to add support for HTML or other markup in the `subject` field, but it is not made this way by design (although [Custom Emojis](../extensions/custom-emojis) are supported) - -An example value for `subject` would be: -```json5 -{ - // ... - "subject": "This is a subject!" - // ... -} -``` - -### Is Sensitive - -| Name | Type | Required | -| :----------- | :----- | :------- | -| is_sensitive | String | No | - -Whether or not the publication contains sensitive content, whether in the content or in attachments or emojis. May be used as a content warning or spoiler warning. If not provided, it is assumed that the publication is not sensitive. - -An example value for `is_sensitive` would be: -```json5 -{ - // ... - "is_sensitive": true - // ... -} -``` - -### Visibility - -| Name | Type | Required | -| :--------- | :--------- | :------- | -| visibility | Visibility | Yes | - -Can be `public`, `unlisted`, `followers`, or `direct`. If not provided, it is assumed that the publication is public. - -- `public`: The publication is visible to everyone, including anonymous users. -- `unlisted`: The publication is visible to everyone, but it should not appear in public timelines or search results. -- `followers`: The publication is visible to followers only. -- `direct`: The publication is a direct message, and is visible only to the mentioned users. - -Servers **MUST** respect the visibility of the publication and **MUST NOT** show the publication to users who are not allowed to see it. - -## Types - -```typescript -interface Publication extends Entity { - type: "Note" | "Patch"; - author: string; - content?: ContentFormat; - category?: CategoryType; - device?: Device; - previews?: LinkPreview[]; - group?: string | "public" | "followers"; - attachments?: ContentFormat[]; - replies_to?: string; - quotes?: string; - mentions?: string[]; - subject?: string; - is_sensitive?: boolean; - visibility: Visibility; - extensions?: Entity["extensions"] & { - "org.lysand:reactions"?: { - reactions: string; - }; - "org.lysand:polls"?: { - poll: { - options: ContentFormat[]; - votes: number[]; // unsigned 64-bit integer - multiple_choice?: boolean; - expires_at: string; - }; - }; - }; -} -``` - -```typescript -enum Visibility { - Public = "public", - Unlisted = "unlisted", - Followers = "followers", - Direct = "direct" -} -``` - -```typescript -interface LinkPreview { - link: string; - title: string; - description?: string; - image?: string; - icon?: string; -} -``` - -```typescript -interface Device { - name: string; - version?: string; - url?: string; -} -``` - -```typescript -/* - * UI examples for each category: - * microblog -> Twitter, Mastodon - * forum -> Reddit - * blog -> Wordpress, WriteFreely - * image -> Instagram - * video -> YouTube - * audio -> SoundCloud, Spotify - * messaging -> Discord, Matrix, Signal - */ -type CategoryType = "microblog" | "forum" | "blog" | "image" | "video" | "audio" | "messaging" -``` diff --git a/docs/objects/server-metadata.md b/docs/objects/server-metadata.md deleted file mode 100644 index 376069d..0000000 --- a/docs/objects/server-metadata.md +++ /dev/null @@ -1,174 +0,0 @@ -# Server Metadata - -Server metadata is metadata that servers can provide to clients to help them determine how to interact with the server. It is meant to be a simple way for servers to provide information to other servers and clients. - -Unlike other objects, server metadata is not meant to be federated. The `id`, `uri` and `created_at` fields are not required on server metadata objects. - -Here is an example server metadata object: -```json5 -{ - "type": "ServerMetadata", - "name": "Example Server", - "version": "1.0.0", - "description": "This is an example server.", - "website": "https://example.com", - "moderators": [ - "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7", - // ... - ], - "admins": [ - // ... - ], - "logo": { - "image/png": { - "content": "https://cdn.example.com/logo.png" - }, - "image/webp": { - "content": "https://cdn.example.com/logo.webp" - } - }, - "banner": { - "image/png": { - "content": "https://cdn.example.com/banner.png" - }, - "image/webp": { - "content": "https://cdn.example.com/banner.webp" - } - }, - "supported_extensions": [ "org.lysand:reactions" ], - "extensions": { - // Example extension - "org.joinmastodon:monthly_active_users": 1000 - } -} -``` - -## Fields - -### Type - -| Name | Type | Required | Notes | -| :--- | :----- | :------- | ------------------------ | -| type | String | Yes | Must be "ServerMetadata" | - -### Name - -| Name | Type | Required | -| :--- | :----- | :------- | -| name | String | Yes | - -Represents the name of the server. The `name` field is required on all Server Metadata objects. This should be the name of the server instance itself, such as "Rosie's Lysand Server", not the software name such as "Mastodon". - -It is recommended that servers limit the length of the name from 1 to 50 characters, but it is up to the server to decide how long the name can be. The protocol does not have an upper limit for the length of the name. - -### Version - -| Name | Type | Required | Notes | -| :------ | :----- | :------- | ----------------------- | -| version | String | Yes | Should be SemVer format | - -Represents the version of the server software. It is recommended that servers use [SemVer](https://semver.org) to version their servers, but it is not required. - -### Description - -| Name | Type | Required | -| :---------- | :----- | :------- | -| description | String | No | - - -This is a short description of this particular server. It should include information about the server, such as what it is about and what it is used for. - -For example, a server focused on a specific topic may include information about that topic in the description. - -It is recommended that servers limit the length of the description from 1 to 500 characters, but it is up to the server to decide how long the description can be. The protocol does not have an upper limit for the length of the description. - -### Website - -| Name | Type | Required | -| :------ | :----- | :------- | -| website | String | No | - -Represents the website of the server. This may be used to link to the server's website, such as a status page or a public modlog. - -### Moderators - -| Name | Type | Required | -| :--------- | :-------------- | :------- | -| moderators | Array of String | No | - -Rrray of URIs to the server moderators. - -If it is not provided, it is assumed that the server does not have any moderators, or is not willing to provide a list. - -### Admins - -| Name | Type | Required | -| :----- | :-------------- | :------- | -| admins | Array of String | No | - -The `admins` field on a Server Metadata object is an array of URIs that represent the admins of the server. - -The `admins` field is not required on all Server Metadata objects. If it is not provided, it is assumed that the server does not have any admins, or is not willing to provide a list. - -### Logo - -| Name | Type | Required | -| :--- | :------------ | :------- | -| logo | ContentFormat | No | - - -The `logo` field on a Server Metadata is a [ContentFormat](../structures/content-format) object. - -The logo content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The logo content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`. - -Lysand heavily recommends that servers provide both the original format and a modern format for each logo, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients. - -Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format. - -> [!NOTE] -> Servers may find it useful to use a CDN that can automatically convert images to modern formats, such as Cloudflare. This will offload image processing from the server, and improve performance for clients. - -### Banner - -| Name | Type | Required | -| :----- | :------------ | :------- | -| banner | ContentFormat | No | - -The `banner` field on a Server Metadata is a [ContentFormat](../structures/content-format) object. - -The banner content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The banner content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`. - -Lysand heavily recommends that servers provide both the original format and a modern format for each banner, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients. - -Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format. - -> [!NOTE] -> Servers may find it useful to use a CDN that can automatically convert images to modern formats, such as Cloudflare. This will offload image processing from the server, and improve performance for clients. - -### Supported Extensions - -| Name | Type | Required | -| :------------------- | :-------------- | :----------------------------- | -| supported_extensions | Array of String | Yes, can be empty array (`[]`) | - -List of extension names that the server supports, in namespaced format (`"org.lysand:reactions"`). - -## Types - -```typescript -interface ServerMetadata { - type: "ServerMetadata"; - name: string; - version: string; - description?: string; - website?: string; - moderators?: string[]; - admins?: string[]; - logo?: ContentFormat; - banner?: ContentFormat; - supported_extensions: string[]; - extensions?: { - [key: string]: object | undefined; - }; -} -``` \ No newline at end of file diff --git a/docs/objects/undo.md b/docs/objects/undo.md deleted file mode 100644 index 0643fb0..0000000 --- a/docs/objects/undo.md +++ /dev/null @@ -1,53 +0,0 @@ -# Undo - -An `Undo` object signifies the reversal of a previously executed action by a user. It is primarily used to revoke an action or remove an existing object. - -An `Undo` object **MUST** contain an `object` field that holds the URI of the object being reversed. - -Servers **MUST NOT** permit users to reverse actions that they did not initiate. - -Upon receiving `Undo` actions, servers **MUST** reverse the action being undone. For instance, if a user expresses liking a post and subsequently undoes the like action, the server **MUST** eliminate the like from the post. Similarly, if an `Undo` action is received for a `Follow` action, the server **MUST** cease following the user. - -If the `Undo` action has a Publication or another object as the `object` field, the server **MUST** discontinue displaying the object to users. It is recommended, but not mandatory, to delete the original object. - -An `Undo` action on a `Patch` object **MUST** be interpreted as the cancellation of the `Note` object, not the patch itself. - -Example: -```json5 -{ - "type": "Undo", - "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", - "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520", - "created_at": "2021-01-01T00:00:00.000Z", - "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19" -} -``` - -## Fields - -### Author - -| Name | Type | Required | -| :----- | :----- | :------- | -| author | String | Yes | - -URI of the [Actor](./actors) who initiated the action. - -### Object - -| Name | Type | Required | -| :----- | :----- | :------- | -| object | String | Yes | - -URI of the object being undone. The object **MUST** be an [Action](./actions) or a [Note](./note). To undo [Patch](./patch) objects, use a subsequent [Patch](./patch) or delete the original [Note](./note). - -## Types - -```typescript -interface Undo extends Entity { - type: "Undo"; - author: string; - object: string; -} -``` \ No newline at end of file diff --git a/docs/objects/user.md b/docs/objects/user.md deleted file mode 100644 index db7f73f..0000000 --- a/docs/objects/user.md +++ /dev/null @@ -1,355 +0,0 @@ -# User - -Users, represented as Actors, are unique entities on the server. They are the sole type of Actor. - -Here is an example user: - -```json5 -{ - "type": "User", - "id": "02e1e3b2-cb1f-4e4a-b82e-98866bee5de7", - "uri": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7", - "created_at": "2021-01-01T00:00:00.000Z", - "display_name": "Gordon Ramsay", - "username": "gordonramsay", - "avatar": { - "image/png": { - "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png", - }, - "image/webp": { - "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.webp", - } - }, - "header": { - "image/png": { - "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png", - }, - "image/webp": { - "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.webp", - } - }, - "indexable": true, - "manually_approves_followers": false, - "public_key": { - "public_key": "...", - "actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" - }, - "bio": { - "text/plain": { - "content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!", - }, - "text/html": { - "content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!", - } - }, - "fields": [ - { - "key": { - "text/plain": { - "content": "Where I live", - } - }, - "value": { - "text/plain": { - "content": "Portland, Oregon", - } - } - } - ], - "featured": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/featured", - "followers": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/followers", - "following": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/following", - "likes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/likes", - "dislikes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/dislikes", - "inbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/inbox", - "outbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/outbox", -} -``` - -## Fields - -All text fields can incorporate [Custom Emojis](../extensions/custom-emojis) as part of the Custom Emojis protocol extension. If a server doesn't support the Custom Emojis extension, no additional work is required to ensure text field compatibility: custom emojis are denoted as part of the plaintext content using `:emoji-name:` syntax, which won't disrupt text formatting. - -### Type - -The `type` of a `User` is invariably `User`. - -### Public Key - -| Name | Type | Required | -| :--------- | :------------------------------------- | :------- | -| public_key | [ActorPublicKeyData](../security/keys) | Yes | - -Author public key. Used to authenticate the actor's identity for their posts. The key **MUST** be encoded in base64. - -All actors **MUST** have a `public_key` field. All servers **SHOULD** authenticate the actor's identity using the `public_key` field, which is used to encode any HTTP requests emitted on behalf of the actor. - -For more information on cryptographic signing, please see the [Signing](/security/signing) page. - -Example of encoding the key in TypeScript: -```ts -// Where keyPair is your key pair -const publicKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey)))); -``` - -### Display Name - -| Name | Type | Required | -| :----------- | :----- | :------- | -| display_name | String | No | - -User's display name. If it is not provided, it is assumed that the actor does not have a display name, and the actor's username should be used instead as a fallback. - -Display names **MUST** be treated as mutable, and **MUST NOT** be used to identify the actor. - -It is recommended that servers limit the display name length from 1 to 50 characters, but the server has the discretion to decide the display name length. The protocol does not impose an upper limit for the display name length. - -### Username - -| Name | Type | Required | -| :------- | :----- | :------- | -| username | String | Yes | - -Actor's username (`@cpluspatch` for example). It is used to loosely identify the actor, and **MUST** be unique across all actors of a server. - -The `username` field **MUST NOT** be used to identify the actor internally or across the protocol. It is only meant to be used as a display name, and as such is mutable by the user. - -The `username` field **MUST** be a string that contains only alphanumeric lowercase characters, underscores, and dashes. It **MUST NOT** contain any spaces or other special characters. - -It **MUST** match this regex: `/^[a-z0-9_-]+$/` - -It is recommended that servers limit the username length from 1 to 20 characters, but the server has the discretion to decide the username length. The protocol does not impose an upper limit for the username length. - -#### Implementation Details - -Usernames are intended to be mutable, but clients should guard this action with warnings and confirmations to prevent accidental changes. Servers should also rate limit username changes to prevent abuse. - -Since user search is done via the username, servers could implement a username history system to be able to find users by their old usernames. However, users might not want to be found by their old usernames, so this feature should be implemented with privacy in mind. - -### Indexable - -| Name | Type | Required | -| :-------- | :------ | :------- | -| indexable | Boolean | Yes | - -Whether or not the actor should be indexed by search engines. - -Servers and search engines should respect the `indexable` field, and **SHOULD NOT** index the actor if the `indexable` field is set to `false`. This is to protect the privacy of users that do not want to be indexed by search engines. - -#### Implementation Details - -This field should also trigger a change in the `robots.txt` file of the server, to prevent search engines from indexing the user's profile, or some other kind of mechanism to prevent indexing (some search engines support using meta tags or headers for example) - -### Manually Approves Followers - -| Name | Type | Required | -| :-------------------------- | :------ | :------- | -| manually_approves_followers | Boolean | Yes | - -Whether or not the actor manually approves followers. This is meant to be used for clients to know if follow requests are automatically accepted or if they need to be manually approved by the user. - -### Avatar - -| Name | Type | Required | -| :----- | :------------ | :------- | -| avatar | ContentFormat | No | - -Profile picture for users. If it is not provided, it is assumed that the actor does not have an avatar. - -The avatar content_type **MUST** be an image format, such as `image/png` or `image/jpeg`. The avatar content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`. - -It is strongly recommended that servers provide both the original format and a modern format for each avatar, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients. - -Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format. - -### Header - -| Name | Type | Required | -| :----- | :------------ | :------- | -| header | ContentFormat | No | - -Banner for users. If it is not provided, it is assumed that the actor does not have a header. - -The header content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The header content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`. - -It is strongly recommended that servers provide both the original format and a modern format for each header, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients. - -Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format. - -### Bio - -| Name | Type | Required | -| :--- | :------------ | :------- | -| bio | ContentFormat | No | - -Used to display a short description of the actor to cleints. It is recommended that servers limit the bio length from 500 to a couple thousand characters, but the server has the discretion to decide the bio length. The protocol does not impose an upper limit for the bio length. - -The `bio` **MUST** be a text format, such as `text/plain` or `text/html`. The `bio` **MUST NOT** be a binary format, such as `image/png` or `video/mp4`. - -An example value for the `bio` field would be: -```json5 -{ - // ... - "bio": { - "text/plain": { - "content": "This is my bio!", - }, - "text/html": { - "content": "This is my bio!", - } - } - // ... -} -``` - -> [!NOTE] -> Lysand heavily recommends that servers support the `text/html` content type, as it is the most rich content type that is supported by most clients. -> -> Lysand also recommends that servers always include a `text/plain` version of each object, as it is the most basic content type that is supported by all clients, such as command line clients. - -It is up to the client to choose which content format to display to the user. The client may choose to display the first content format that it supports, or it may choose to display the content format that it thinks is the most appropriate. - -### Fields - -| Name | Type | Required | -| :----- | :------------- | :------- | -| fields | Array of Field | No | - -Custom key-value pairs for clients, such as additional metadata. If not provided, it is assumed that the actor does not have any fields. - -An example value for the `fields` field would be: -```json5 -{ - // ... - "fields": [ - { - "key": { - "text/plain": { - "content": "Where I live", - } - }, - "value": { - "text/plain": { - "content": "Portland, Oregon", - } - } - } - // Other fields... - ] - // ... -} -``` - -Fields are formatted as follows: -```ts -interface Field { - key: ContentFormat; - value: ContentFormat; -} -``` - -Both `key` and `value` should be presented to the user as a pair. - -The `key` and `value` fields **MUST** be text formats, such as `text/plain` or `text/html`. They **MUST NOT** be binary formats, such as `image/png` or `video/mp4`. - -### Featured - -| Name | Type | Required | -| :------- | :----- | :------- | -| featured | String | Yes | - -Please refer to [Featured Publications](../federation/endpoints) for more information. - -### Followers - -| Name | Type | Required | -| :-------- | :----- | :------- | -| followers | String | Yes | - -Please refer to [User Followers](../federation/endpoints) for more information. - -### Following - -| Name | Type | Required | -| :-------- | :----- | :------- | -| following | String | Yes | - -Please refer to [User Following](../federation/endpoints) for more information. - -### Likes - -| Name | Type | Required | -| :---- | :----- | :------- | -| likes | String | Yes | - -Please refer to [User Likes](../federation/endpoints) for more information. - -### Dislikes - -| Name | Type | Required | -| :------- | :----- | :------- | -| dislikes | String | Yes | - -Please refer to [User Dislikes](../federation/endpoints) for more information. - -### Inbox - -| Name | Type | Required | -| :---- | :----- | :------- | -| inbox | String | Yes | - -The `inbox` field on an Actor is a string that displays the URI of the actor's inbox. It is used to identify the actor's inbox for federation. - -Please refer to [Inbox](../federation/endpoints) for more information. - -### Outbox - -| Name | Type | Required | -| :----- | :----- | :------- | -| outbox | String | Yes | - -The `outbox` field on an Actor is a string that displays the URI of the actor's outbox. It is used to identify the actor's outbox for federation. - -Please refer to [Outbox](../federation/endpoints) for more information. - -## Related Extensions - -These extensions might add or affect the User object if used: -- [Custom Emojis](../extensions/custom-emojis) -- [Vanity Profile](../extensions/vanity) - -## Types - -```typescript -interface User extends Entity { - type: "User"; - id: string; - uri: string; - created_at: string; - display_name?: string; - username: string; - avatar?: ContentFormat; - header?: ContentFormat; - indexable: boolean; - public_key: ActorPublicKeyData; - bio?: ContentFormat; - fields?: Field[]; - featured: string; - followers: string; - following: string; - likes: string; - dislikes: string; - inbox: string; - outbox: string; - extensions?: Entity["extensions"] & { - "org.lysand:vanity"?: VanityExtension; - }; -} -``` - -```typescript -interface Field { - key: ContentFormat; - value: ContentFormat; -} -``` \ No newline at end of file diff --git a/docs/public/assets/boosting.png b/docs/public/assets/boosting.png deleted file mode 100644 index 94fd6e8f63f850d5b73229179820784f7b684c78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43568 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 G`2H90hN5Z! diff --git a/docs/public/assets/discord-buttons.webp b/docs/public/assets/discord-buttons.webp deleted file mode 100644 index 593b25a212b0223005a147e5da0d474db7a07320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31498 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 z z!+1u=e==_AXNC*ge5rjb{OK3`8azeizx4Y`Db&fJ&mxKC=9Cv&C~9ly&tU1+nH@oX zm8Q3FY&4-69`b>)Np*mm+gR|o0zgZmjayP)xw-G};4r3!{fpGFSGA5&{q(pE<2Uo{ zMw%Zwts&-K3STjy^u6F~r_JC7XS2n^l!Yf8>R4Lq!RK425hADUSJ3afuszQ)J1zx4nGs5WC|ocraPKH~A-~A<4w;LaNNEw$d^9r9wKj*d3uQ+b_(W!3A*@b$UPmpw=Nv;;kq(g{YR; z9`GPVe6Vhh9fo{Q_aj@Q+-0xANitxl7?Tps6x)u+^`2iP zI?mC?fkqY5iy3f-SCC()g$v8Xme$15Yqr{Dy_#pf^=7tIL;mdlO$J@-9imhX`sQA{ zZq~;!*v&C$NO0J7k1dH?#!kC*Y<^YQ=VSMe7vibH<)OK?v=2@I{BNfNy>zi#oET?L z%z;R<@n`()^3LNLN8My82yBI50nRwiK)FtL|>>)pR z$Uc?*c3LF6cB_H={Pb8FLQw%9hY_(;Nyx2#+PHt{rM=(ONfN_>g!ELPb<^Ic@wqCe zHynKaK4$%-mSjHLp^H31$gZZzQuIChx|>rEw7qFO4sUzd(UJz+t!7=EnW`%N%lFL2 z4QA=qZmy+ge>&KxZ6tkZWD8e|c$FRcFSCU$Dtcx+nbou%zFIa`5;Pu0ub1%8Sa$URW6 zUk}9GLp>-)dY32m^X?cOiEX_VVrUX>r}aojPc>hK!v99Jud&BXu1zm39+iDMm>{;T zhQ5h-+qSyd@??&f5FomI9UoyJz8u#uNAO5@x=5U|v3~2G8*yVcn?bAjSLMl8{CS>C z$I=f0P1L_msc=)5aBobwk2W^chf077Tt!t8uBwQDJHX-ERHz~3|KH+a- diff --git a/docs/security/api.md b/docs/security/api.md deleted file mode 100644 index 10b9427..0000000 --- a/docs/security/api.md +++ /dev/null @@ -1,116 +0,0 @@ -# API Security - -This document details the security requirements for Lysand API implementations. - -It is a **MUST** for all Lysand-compatible servers to adhere to the guidelines marked as `Server API`. - -The guidelines marked as `Client API` are optional but recommended for client software. - -## Server API - -**Server API routes** are the endpoints of the server used by federation. These endpoints must **ONLY** be accessible by other servers and not by client software. - -> [!NOTE] -> You may notice that most of these guidelines are redundant or useless for a simple JSON API system. However, they are mandated to encourage good security practices, so that developers don't overlook them on the important Client API routes. - -### HTTP Security - -All HTTP requests/responses **MUST** be transmitted using the **Hypertext Transfer Protocol Secure Extension** (HTTPS). Servers **MUST NOT** send responses without TLS (HTTPS), except for development purposes (e.g., if a server is operating on localhost or another local network). - -Servers should support HTTP/2 and HTTP/3 for enhanced performance and security. Servers **MUST** support HTTP/1.1 at a minimum, however TLS 1.2 is not allowed. - -Additionally, IPv6 is **RECOMMENDED** for all servers for enhanced security and performance. In the (far away) future, IPv4 will be removed, and servers that do not support IPv6 may face connectivity issues. (Whenever possible, servers should support both IPv4 and IPv6.) - -### Content Security Policy - -Servers **MUST** set a Content Security Policy (CSP) header to all their Server API routes to prevent XSS attacks. The CSP must be as restrictive as possible: - -``` -Content-Security-Policy: default-src 'none'; frame-ancestors 'none' -``` - -### Security headers - -Servers **MUST** set the following security headers to all their Lysand API routes: - -``` -X-Content-Type-Options: nosniff -X-Frame-Options: DENY -Referrer-Policy: no-referrer -Strict-Transport-Security: max-age=31536000; -``` - -## Object Storage - -Object storage may be abused to store fake Lysand objects if the object storage is on the same origin as the server. To prevent this, servers must sign all valid objects with the author's private key, in the same way as described in the [Signing](signing.md) spec for outbound requests. This signature **MUST** be verified by any requesting server before accepting the object. - -This behaviour is also documented in the [Signing](signing.md) spec and [general spec](../spec.md). It is duplicated here in case you missed it the first time. - -## Client API - -**Client API routes** are the endpoints of the server used by client software. These endpoints must **ONLY** be accessible by client software and not by other servers. As an example, the [Mastodon API](https://docs.joinmastodon.org/api/) is a Client API. - -### Rate Limiting - -Servers **SHOULD** implement rate limiting on all Client API routes to prevent abuse. The rate limit **SHOULD** be set to a reasonable value, such as 100 requests per minute per IP address. This is left to the server administrator's discretion. - -### Authentication - -Client API routes **SHOULD** require authentication to prevent unauthorized access. The authentication method **SHOULD** be OAuth 2.0, as it is a widely-used and secure authentication method. - -Servers should also use either cryptographically secure random access tokens (via OAuth 2.0) or JWTs for authentication. The access tokens **MUST** be stored securely and **MUST NOT** be exposed to unauthorized parties. - -### Content Security Policy - -Servers **SHOULD** set a Content Security Policy (CSP) header to all their Client API routes to prevent XSS attacks. The CSP must be as restrictive as possible. - -No example is provided here, as this specification does not mandate a specific client API for servers. - -### Security headers - -Servers **SHOULD** set the following security headers to all their Client API routes: - -``` -X-Content-Type-Options: nosniff -X-Frame-Options: DENY -Referrer-Policy: no-referrer -``` - -If the server supports CORS, the `Access-Control-Allow-Origin` header **SHOULD** be set (usually to `*`), and the `Access-Control-Allow-Methods` header **SHOULD** be set to the allowed methods. - -`Permissions-Policy` headers are **RECOMMENDED** for all Client API routes that serve JS/HTML content (the "frontend"). The permissions policy should be as restrictive as possible. - -## Security Considerations - -When implementing security in your server, it is important to consider the following security considerations: - -### Authentication - -- Tokens/JWTs should expire after a reasonable amount of time (e.g., a week) to prevent unauthorized access. Additionally, they should be invalidated after a user logs out or changes their password. -- Passwords **SHOULD** be hashed using a secure hashing algorithm, such as Argon2 or bcrypt. They **SHOULD NOT** be stored in plaintext or using weak hashing algorithms such as MD5 or SHA-1. Be also aware of weak default rounds for these algorithms. -- Servers **SHOULD** implement multi-factor authentication (MFA) to provide an additional layer of security for users. - - Passkeys/WebAuthn are **RECOMMENDED** for MFA, as they are more secure than SMS or email-based MFA. -- Servers **SHOULD** implement very strict rate limiting on login attempts to prevent brute force attacks. -- CSRF tokens **SHOULD** be used to prevent CSRF attacks on sensitive endpoints. - -### Key Management - -- Ensure that private keys are stored securely and are not exposed to unauthorized parties. -- Allow exporting private keys by users in secure formats, such as encrypted files. Do not allow exporting private keys to untrusted environments. Additionally, indicate that this is a security-sensitive operation. - -> [!NOTE] -> The importation of private keys is not recommended, as it can lead to security issues. However, if you choose to implement this feature, warn any users that this is probably a bad idea. - -### Cryptography - -- Do not roll your own security, but instead use well-established libraries such as the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). -- Cryptographic libraries written in unsafe languages, such as C, or that are a frequent source of security issues (e.g., OpenSSL) should be avoided. -- Configure your server to only accept TLS 1.3 or higher, as older versions of TLS are vulnerable to attacks. - -### General Security - -- Have your server regularly audited for security vulnerabilities by professionals. -- Keep all packages, dependencies, and libraries up-to-date. This also includes OS libraries (OSes that don't update packages often except for security patches such as Debian can be a risk, as often times a lot of vulnerabilities are missed). -- Consider providing a container image for your server that does not run as the root user, and has all the necessary security configurations in place. -- Open-source your server software, as it allows for more eyes on the code and can help identify security vulnerabilities faster. - diff --git a/docs/security/keys.md b/docs/security/keys.md deleted file mode 100644 index 04d3597..0000000 --- a/docs/security/keys.md +++ /dev/null @@ -1,51 +0,0 @@ -# Public Key Cryptography - -Lysand employs public key cryptography for object signing, ensuring the authenticity of the object's origin. - -All public keys in Lysand **MUST** be encoded using the [ed25519](https://ed25519.cr.yp.to/) algorithm. This algorithm is favored due to its speed, security, and compact key size. Legacy algorithms such as RSA are not supported and **SHOULD NOT** be implemented using extensions due to security considerations. - -While it's technically possible to implement other encryption algorithms using extensions, it's generally discouraged. - -In the near future, Lysand will also support quantum-resistant algorithms, once they are incorporated into popular libraries. - -Here's an example of generating a public-private key pair in TypeScript using the WebCrypto API: - -```ts -const keyPair = await crypto.subtle.generateKey( - "Ed25519", - true, - ["sign", "verify"] -); - -// Encode both to base64 (Buffer is a Node.js API, replace with btoa and atob for browser environments) -const privateKey = Buffer.from( - await crypto.subtle.exportKey("pkcs8", keys.privateKey), -).toString("base64"); - -const publicKey = Buffer.from( - await crypto.subtle.exportKey("spki", keyPair.publicKey), -).toString("base64"); - -// Store the public and private key somewhere in your user data -``` - -> [!WARNING] -> Support for Ed25519 in the WebCrypto API is a recent addition and may not be available in some older runtimes, such as Node.js or older browsers. - -Public key data is represented as follows across the protocol: - -```ts -interface ActorPublicKeyData { - public_key: string; - actor: string; -} -``` - -The `public_key` field is a string that contains the user's public key. It **MUST** be encoded using base64. - -Base64 encoding of public and private keys is defined as follows: -- The public key **MUST** be encoded using the `spki` format. -- The private key **MUST** be encoded using the `pkcs8` format. -- Both keys **MUST** be turned from raw bytes to base64 by turning the bytes into a sequence of UTF-16 code units, then encoding them as base64 (as shown in the example above). - -The `actor` field is a string that contains the user's URI. This field is mandatory. \ No newline at end of file diff --git a/docs/security/signing.md b/docs/security/signing.md deleted file mode 100644 index 27c1f8c..0000000 --- a/docs/security/signing.md +++ /dev/null @@ -1,184 +0,0 @@ -# HTTP Signatures - -Lysand employs cryptography to safeguard objects from being altered during transit. This is achieved by signing objects using a private key, and then verifying the signature with a corresponding public key. - -> [!NOTE] -> The 'author' of the object refers to the entity (usually an [Actor](../objects/actors)) that created the object. This is indicated by the `author` property on the object body. - -> [!NOTE] -> Please see the [API Security](api.md) document for security guidelines. - -## Creating a Signature - -Prerequisites: -- A private key for the author of the object. -- The object to be signed, serialized as a string. - -### Signature - -The `Signature` is a string, typically sent as part of the `Signature` HTTP header. It contains a signed string signed with a private key. - -It is formatted as follows: -``` -Signature: keyId="$0",algorithm="ed25519",headers="(request-target) host date digest",signature="$1" -``` - -- `$0` is the URI of the user that is sending the request. (e.g., `https://example.com/users/uuid`) -- `$1` is the base64-encoded signed string. - -The signed string is calculated as follows: - -1. Create a string that contains the following, replacing the placeholders with the actual values of the request: -``` -(request-target): post $2 -host: $3 -date: $4 -digest: SHA-256=$5 -``` - -- `$2` is the path of the request (e.g., `/users/uuid/inbox`). -- `$3` is the host of the server that is receiving the request. -- `$4` is the date and time that the request was sent (ISO 8601, e.g. `2024-04-10T01:27:24.880Z`). -- `$5` is the SHA-256 digest of the request body, base64-encoded. - -> [!WARNING] -> The last line of the signed string **MUST** be terminated with a newline character (`\n`). - -2. Sign the string with the user's private key. - -2. Base64-encode the signature. - -#### Example - -Let's imagine a user at `sender.com` wants to send something to a user at `receiver.com`'s inbox. - -Here is an example of signing a request using TypeScript and the WebCrypto API. - -```typescript -const privateKey = ... // CryptoKey -const body = {...} // Object to be signed -const date = new Date(); - -const digest = await crypto.subtle.digest( - "SHA-256", - // Make sure to follow the JSON object handling guidelines - // This just uses JSON.stringify as an example - new TextEncoder().encode(JSON.stringify(body)), -); - -const userInbox = new URL( - "https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox" -); - -const date = new Date(); - -// Note: the Buffer class is from the Node.js Buffer API, this can be replaced with btoa and atob magic in the browser -const signature = await crypto.subtle.sign( - "Ed25519", - privateKey, - new TextEncoder().encode( - `(request-target): post ${userInbox.pathname}\n` + - `host: ${userInbox.host}\n` + - `date: ${date.toISOString()}\n` + - `digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString( - "base64", - )}\n`, - ), -); - -const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString( - "base64", -); -``` - -> [!WARNING] -> Support for Ed25519 in the WebCrypto API is recent and may not be available in some older runtimes, such as Node.js or older browsers. - -The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows: -```ts -await fetch("https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox", { - method: "POST", - headers: { - "Content-Type": "application/json", - Date: date.toISOString(), - Origin: "sender.com", - Signature: `keyId="https://sender.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`, - }, - // Once again, make sure to follow the JSON object handling guidelines - body: JSON.stringify(body), -}); -``` - -Example of validation on the receiving server side: - -```typescript -// req is a Request object -const signatureHeader = req.headers.get("Signature"); -const origin = req.headers.get("Origin"); -const date = req.headers.get("Date"); - -if (!signatureHeader) { - return errorResponse("Missing Signature header", 400); -} - -if (!origin) { - return errorResponse("Missing Origin header", 400); -} - -if (!date) { - return errorResponse("Missing Date header", 400); -} - -const signature = signatureHeader - .split("signature=")[1] - .replace(/"/g, ""); - -const digest = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode(JSON.stringify(body)), -); - -const keyId = signatureHeader - .split("keyId=")[1] - .split(",")[0] - .replace(/"/g, ""); - -// TODO: Fetch sender using WebFinger if not found -const sender = ... // Get sender from your database via its URI (inside the keyId variable) - -const public_key = await crypto.subtle.importKey( - "spki", - // Buffer is a Node.js API, this can be modified to work in browser too - Buffer.from(sender.publicKey, "base64"), - "Ed25519", - false, - ["verify"], -); - - -const expectedSignedString = - `(request-target): ${req.method.toLowerCase()} ${ - new URL(req.url).pathname - }\n` + - `host: ${new URL(req.url).host}\n` + - `date: ${date}\n` + - `digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString( - "base64", - )}\n`; - -// Check if signed string is valid -const isValid = await crypto.subtle.verify( - "Ed25519", - public_key, - Buffer.from(signature, "base64"), - new TextEncoder().encode(expectedSignedString), -); - -if (!isValid) { - throw new Error("Invalid signature"); -} -``` - -Signature is **REQUIRED** on **ALL** outbound and inbound requests. If the request is not signed, the server **MUST** respond with a `401 Unauthorized` response code. However, the receiving server is not required to validate the signature, it just must be provided. - -If a request is made by the server and not by a user, the [Server Actor](/federation/server-actor) **MUST** be used in the `author` field. \ No newline at end of file diff --git a/docs/spec.md b/docs/spec.md deleted file mode 100644 index b1b92dc..0000000 --- a/docs/spec.md +++ /dev/null @@ -1,102 +0,0 @@ -# Introduction - -> [!NOTE] -> You are looking at the documentation for `Lysand 3.1`, released in July 2024 -> -> Small changes may still be made before the final release. -> -> Previous versions: -> - `Lysand 3.0`, released in May 2024. -> - `Lysand 2.0`, released in March 2024. -> - `Lysand 1.0`, published in September 2023. - -The Lysand Protocol is designed as a communication medium for federated applications, leveraging the HTTP stack. Its simplicity ensures ease of implementation and comprehension. - -Distinct from ActivityPub, Lysand incorporates an extensive range of built-in features tailored for social media applications. It prioritizes security and privacy by default. - -Lysand aims for standardization, discouraging vendor-specific implementations, as seen in Mastodon's adaptation of ActivityPub. It relies on straightforward JSON objects and HTTP requests, eliminating the need for complex serialization formats. - -This repository provides TypeScript types for every object in the protocol, facilitating easy adaptation to other languages. - -# Design Goals - -While Lysand draws parallels with popular protocols like ActivityPub and ActivityStreams, it is not compatible with either. It also does not support ActivityPub's JSON-LD serialization format. - -Lysand-compatible servers may choose to implement other protocols, such as ActivityPub, but it is not a requirement. - -# Vocabulary - -The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used in this document as defined in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). - -- **Actor**: An individual or entity utilizing the Lysand protocol, analogous to ActivityPub's `Actor` objects. An actor could be a [Server Actor](federation/server-actor), representing a server, or a [User Actor](objects/actors). -- **Server**: A server that deploys the Lysand protocol, referred to as an **implementation**. Servers are also known as **instances** when referring to the deployed form. -- **Entity**: A generic term for any object in the Lysand protocol, such as an [Actor](objects/actors), [Note](objects/publications), or [Like](objects/like). - -# Implementation Requirements - -Servers **MUST** reject any requests that fail to respect the Lysand specification in any way. This includes, but is not limited to, incorrect JSON object handling, incorrect HTTP headers, and incorrect URI normalization. - -## For strictly-typed languages (e.g. Rust) - -All numbers are to be treated as 64-bit integer or floats (depending on whether a valid value would be int or float). If a valid value cannot be negative, it must also be treated as unsigned. - -Examples: -- A `size` (bytes) property on a file object should be treated as an unsigned 64-bit integer. -- A `duration` property on a video object should be treated as an unsigned 64-bit float. - -## Optional Fields - -Fields marked as "optional" may be set to `null` or omitted entirely. If a field is omitted, it is assumed to be `null`, unless it is not in the object's schema. - -## HTTP - -All HTTP request and response bodies **MUST** be encoded as UTF-8 JSON, with the `Content-Type` header set to `application/json; charset=utf-8`. Appropriate signatures must be included in the `Signature` header for **every request and response**. - -Servers **MUST** use UUIDs or a UUID-compatible system for the `id` field. Any valid UUID is acceptable, but it **should** be unique across the entire known network if possible. However, uniqueness across the server is the only requirement. - -> [!NOTE] -> Protocol implementers may prefer [UUIDv7](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) over the popular UUIDv4 for their internal databases, as UUIDv7 is lexicographically sortable by time generated. A PostgreSQL extension is available [here](https://github.com/fboulnois/pg_uuidv7). - -All URIs **MUST** be absolute and HTTPS, except for development purposes. They **MUST** be unique across the entire network and **must not** contain mutable data, such as the actor's `username`. - -All URIs **MUST** be normalized and **MUST NOT** contain any query parameters, except where explicitely allowed. URI normalization is defined in [RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6). - -### Requests - -All requests **MUST** include at least the following headers: -- `Accept: application/json` -- `Content-Type: application/json; charset=utf-8` if the request contains a body -- `Signature` if the request body is signed (which is typically the case) - -Additionally, requests **SHOULD** include the following headers (though not mandated by the protocol): -- `User-Agent` with a value that identifies the client software - -### Responses - -All responses **MUST** include at least the following headers: -- `Content-Type: application/json; charset=utf-8` if the response contains a body -- `Signature` if the response body is signed (which is typically the case) -- `Cache-Control: no-store` on entities that can be edited directly without using a [Patch](objects/patch), such as [Actors](objects/actors) -- A cache header with a `max-age` of at least 5 minutes for entities that are not expected to change frequently, such as [Notes](objects/publications) -- A cache header with a large `max-age` for media files when served by a CDN or other caching service under the server's control - - -## JSON Object Handling - -All JSON objects disseminated during federation **MUST** be handled as follows: -- The object's keys **MUST** be arranged in lexicographical order. -- The object **MUST** be serialized using the [Canonical JSON](https://datatracker.ietf.org/doc/html/rfc8785) format. -- The object **MUST** be encoded using UTF-8. -- The object **MUST** be signed using either the [Server Actor](federation/server-actor) or the [Actor](objects/actors) object's private key, depending on the context. (Signatures and keys are governed by the rules outlined in the [Keys](security/keys) and [Signing](security/signing) spec). Signatures are encoded using request/response headers, not within the JSON object itself. - -## API Security - -All servers **MUST** adhere to the security guidelines outlined in the [API Security](security/api) document. - ---- - -## Appendix - -> [This document is dedicated to all citizens of planet Earth. You deserve freedom of communication; we hope we have contributed in some part, however small, towards that goal and right.](https://w3c.github.io/activitypub/#acknowledgements) - -Signed by the maintainers. diff --git a/docs/structures/collection.md b/docs/structures/collection.md deleted file mode 100644 index 9b698e4..0000000 --- a/docs/structures/collection.md +++ /dev/null @@ -1,33 +0,0 @@ -# Collections - -Collections are structured JSON objects that encapsulate a group of related items. They are typically used to represent a set of similar objects, such as a series of publications or a group of users. - -Here's how a Collections can be represented in TypeScript: - -```ts -interface Collections { - first: string; - last: string; - total_count: number; // unsigned 64-bit integer - author: string; - next?: string; - prev?: string; - items: T[]; -} -``` - -Collections are intended to be served in a paginated format, with a set of items and links to the next and previous sets of items. For example, if a user has a collection of Notes that is several thousand items long, the server can serve the first 50 items when requested and provide a link to the next 50 items. - -Collections **MUST** include: -- A `first` field that holds the URI of the first page of collection items, and a `last` field that holds the URI of the last page of collection items. In the case that there is only one page, `first` and `last` should be the same. -- A `total_count` field that holds the total number of items in the set, across all pages. -- A `next` field that holds the URI of the next set of items, and a `prev` field that holds the URI of the previous set of items. These fields are optional if the user is on the first or last set of items. -- An `items` field that holds a paginated array of items in the set. (for example, a series of publications or a group of users) -- An `author` field that holds the URI of the entity that created the set (such as the [Actor](../objects/actors) that creates posts). This is used to identify the creator of the set for signing. If the set is generated by the server and not by a specific user (such as the Endorsement set with the [ServerEndorsement Extension](/extensions/server-endorsement)), the `author` should be the server actor's URI. - -`first`, `last`, `prev` and `next` URIs may use query strings to specify the range of items to be returned. For example, a `first` URI may look like `https://example.com/users/uuid/notes?start=0&end=50`. - -It is recommended that pages are limited to a hundred elements at most, but also at least twenty elements, so as to not overload federation with large payloads. - -> [!WARNING] -> Like any other payload, Collections are to be signed using the author's private key. Unsigned collections are not valid. \ No newline at end of file diff --git a/docs/structures/content-format.md b/docs/structures/content-format.md deleted file mode 100644 index baf5ed5..0000000 --- a/docs/structures/content-format.md +++ /dev/null @@ -1,100 +0,0 @@ -# ContentFormat - -The `ContentFormat` structure, as represented below in TypeScript, is a flexible and robust way to handle various types of content. It is designed to accommodate a wide range of content types and provide additional metadata about the content. - -```ts -interface ContentFormat { - [contentType: string]: { - content: string; - description?: string; - size?: number; // unsigned 64-bit integer - hash?: { - sha256?: string; - sha512?: string; - [key: string]: string | undefined; - }; - blurhash?: string; - fps?: number; // unsigned 64-bit integer - width?: number; // unsigned 64-bit integer - height?: number; // unsigned 64-bit integer - duration?: number; // unsigned 64-bit float - } -} -``` - -Here's an example of how this structure can be used: - -```json -{ - "text/plain": { - "content": "Hello, world!" - } -} -``` - -And another example: - -```json -{ - "image/png": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png", - "description": "A jolly horse running through mountains", - "size": 123456, - "hash": { - "sha256": "91714fc336210d459d4f9d9233de663be2b87ffe923f1cfd76ece9d06f7c965d" - } - } -} -``` - -The `content` field holds the actual content of the object. This content can be either a string containing plaintext, or a URI to the actual content when it cannot be encoded as this format. - -The `description` field provides a brief summary of the content. This is particularly useful for accessibility purposes, such as for visually impaired users, or when the content fails to load. It's an optional field, and if not provided, it's assumed that the content doesn't have a description. - -The `size` field, also optional, indicates the size of the content in bytes. While it's not necessary for text content, it's recommended for binary content like images, videos, and audio. This information helps clients decide whether to download the content based on its size. - -The `ContentFormat` structure is designed to handle multiple formats of the same file. For instance, a PNG image and a WebP image. However, it's not intended for formats that can't be converted to others, like PDFs. These should only be stored once. - -Here's an acceptable use case: - -```json -{ - "image/png": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png", - "description": "A jolly horse running through mountains", - "hash": { - "sha256": "91714fc336210d459d4f9d9233de663be2b87ffe923f1cfd76ece9d06f7c965d" - } - }, - "image/webp": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp", - "description": "A jolly horse running through mountains", - "hash": { - "sha256": "b493d48364afe44d11c0165cf470a4164d1e2609911ef998be868d46ade3de4e" - } - } -} -``` - -However, this is not: - -```json -{ - "image/png": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png", - "description": "A jolly horse running through mountains" - }, - "image/webp": { - "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp", - "description": "A jolly horse running through mountains" - }, - "application/pdf": { - "content": "https://cdn.example.com/attachments/anotherfile.pdf", - "description": "An informative PDF document on macroeconomics" - } -} -``` - -Each `ContentFormat` object should be treated as a **single file in multiple optional formats**, not as multiple files. The multiple formats are intended to optimize bandwidth usage. - -If optional fields are provided for one object in the `ContentFormat`, they should be provided for all objects in the `ContentFormat`. For instance, if the `description` field is provided for one object, it should be provided for all objects, as they represent the same file. \ No newline at end of file diff --git a/docs/structures/custom-emoji.md b/docs/structures/custom-emoji.md deleted file mode 100644 index 0c3ae44..0000000 --- a/docs/structures/custom-emoji.md +++ /dev/null @@ -1,42 +0,0 @@ -# Custom Emojis in Lysand - -Lysand supports the use of custom emojis. Here's how they are represented in TypeScript: - -```ts -interface Emoji { - name: string; - alt?: string; - url: ContentFormat; -} -``` - -Custom emojis in Lysand are part of an optional extension to the protocol. For more details, refer to the [Protocol Extensions](../extensions) section. - -While servers have the discretion to implement custom emojis, it is highly recommended for a richer user interaction. - -Here's an example of a custom emoji representation: - -```json -{ - "name": "happy_face", - "alt": "A happy face emoji.", - "url": { - "image/webp": { - "content": "https://cdn.example.com/emojis/happy_face.webp", - } - } -} -``` - -The `name` field is a string that should only contain alphanumeric characters, underscores, and dashes. Spaces or other special characters are not allowed. It should match the following regex: `/^[a-zA-Z0-9_-]+$/`. - -The `url` field is a [ContentFormat](./content-format), serving as a list of URLs where the emoji can be accessed. It is mandatory for all emojis and should contain at least one URL. The `url` field should be a binary image format, such as `image/png` or `image/jpeg`. Text formats like `text/plain` or `text/html` are not acceptable. - -The `alt` field is an optional string that provides the alt text for the emoji. This is particularly useful for visually impaired users or when the emoji fails to load. If not provided, it's assumed that the emoji doesn't have an alt text. - -While emojis are typically small and don't consume much bandwidth, servers may choose to transcode emojis into more modern formats like WebP, AVIF, JXL, or HEIF for optimization. Clients should display the most modern format they support. If they don't support any modern formats, they should display the original format. - -> [!NOTE] -> Servers might find it beneficial to use a CDN like Cloudflare that can automatically convert images to modern formats. This approach offloads image processing from the server and enhances performance for clients. - -The size of emojis is not standardized and is left to the server's discretion. Servers may choose to limit the size of emojis, but it's not mandatory. As a general guideline, an upper limit of a few hundred kilobytes is recommended to avoid excessive bandwidth usage. \ No newline at end of file diff --git a/images/logos/go.svg b/images/logos/go.svg new file mode 100644 index 0000000..7f7b19d --- /dev/null +++ b/images/logos/go.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/images/logos/node.svg b/images/logos/node.svg new file mode 100644 index 0000000..1d09de2 --- /dev/null +++ b/images/logos/node.svg @@ -0,0 +1,4 @@ + + + diff --git a/images/logos/php.svg b/images/logos/php.svg new file mode 100644 index 0000000..0a9ac46 --- /dev/null +++ b/images/logos/php.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/images/logos/python.svg b/images/logos/python.svg new file mode 100644 index 0000000..9bceb58 --- /dev/null +++ b/images/logos/python.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/images/logos/ruby.svg b/images/logos/ruby.svg new file mode 100644 index 0000000..b22a5bf --- /dev/null +++ b/images/logos/ruby.svg @@ -0,0 +1,4 @@ + + + diff --git a/lib/remToPx.ts b/lib/remToPx.ts new file mode 100644 index 0000000..48e2748 --- /dev/null +++ b/lib/remToPx.ts @@ -0,0 +1,10 @@ +export function remToPx(remValue: number) { + const rootFontSize = + typeof window === "undefined" + ? 16 + : Number.parseFloat( + window.getComputedStyle(document.documentElement).fontSize, + ); + + return remValue * rootFontSize; +} diff --git a/mdx-components.tsx b/mdx-components.tsx new file mode 100644 index 0000000..4688405 --- /dev/null +++ b/mdx-components.tsx @@ -0,0 +1,10 @@ +import type { MDXComponents } from "mdx/types"; + +import * as mdxComponents from "./components/mdx"; + +export function useMDXComponents(components: MDXComponents) { + return { + ...components, + ...mdxComponents, + }; +} diff --git a/mdx/recma.mjs b/mdx/recma.mjs new file mode 100644 index 0000000..237ac11 --- /dev/null +++ b/mdx/recma.mjs @@ -0,0 +1,3 @@ +import { mdxAnnotations } from "mdx-annotations"; + +export const recmaPlugins = [mdxAnnotations.recma]; diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs new file mode 100644 index 0000000..66999d1 --- /dev/null +++ b/mdx/rehype.mjs @@ -0,0 +1,129 @@ +import { slugifyWithCounter } from "@sindresorhus/slugify"; +import * as acorn from "acorn"; +import { toString as mdastToString } from "mdast-util-to-string"; +import { mdxAnnotations } from "mdx-annotations"; +import shiki from "shiki"; +import { visit } from "unist-util-visit"; + +function rehypeParseCodeBlocks() { + return (tree) => { + // biome-ignore lint/style/useNamingConvention: + visit(tree, "element", (node, _, parentNode) => { + if (node.tagName === "code" && node.properties.className) { + parentNode.properties.language = + node.properties.className[0]?.replace(/^language-/, ""); + } + }); + }; +} + +let highlighter; + +function rehypeShiki() { + return async (tree) => { + highlighter = + highlighter ?? + (await shiki.getHighlighter({ theme: "css-variables" })); + + visit(tree, "element", (node) => { + if ( + node.tagName === "pre" && + node.children[0]?.tagName === "code" + ) { + const codeNode = node.children[0]; + const textNode = codeNode.children[0]; + + node.properties.code = textNode.value; + + if (node.properties.language) { + const tokens = highlighter.codeToThemedTokens( + textNode.value, + node.properties.language, + ); + + textNode.value = shiki.renderToHtml(tokens, { + elements: { + pre: ({ children }) => children, + code: ({ children }) => children, + line: ({ children }) => `${children}`, + }, + }); + } + } + }); + }; +} + +function rehypeSlugify() { + return (tree) => { + const slugify = slugifyWithCounter(); + visit(tree, "element", (node) => { + if (node.tagName === "h2" && !node.properties.id) { + node.properties.id = slugify(mdastToString(node)); + } + }); + }; +} + +function rehypeAddMDXExports(getExports) { + return (tree) => { + const exports = Object.entries(getExports(tree)); + + for (const [name, value] of exports) { + for (const node of tree.children) { + if ( + node.type === "mdxjsEsm" && + new RegExp(`export\\s+const\\s+${name}\\s*=`).test( + node.value, + ) + ) { + return; + } + } + + const exportStr = `export const ${name} = ${value}`; + + tree.children.push({ + type: "mdxjsEsm", + value: exportStr, + data: { + estree: acorn.parse(exportStr, { + sourceType: "module", + ecmaVersion: "latest", + }), + }, + }); + } + }; +} + +function getSections(node) { + const sections = []; + + for (const child of node.children ?? []) { + if (child.type === "element" && child.tagName === "h2") { + sections.push(`{ + title: ${JSON.stringify(mdastToString(child))}, + id: ${JSON.stringify(child.properties.id)}, + ...${child.properties.annotation} + }`); + } else if (child.children) { + sections.push(...getSections(child)); + } + } + + return sections; +} + +export const rehypePlugins = [ + mdxAnnotations.rehype, + rehypeParseCodeBlocks, + rehypeShiki, + rehypeSlugify, + [ + rehypeAddMDXExports, + (tree) => ({ + sections: `[${getSections(tree).join()}]`, + }), + ], +]; diff --git a/mdx/remark.mjs b/mdx/remark.mjs new file mode 100644 index 0000000..ea1a1e7 --- /dev/null +++ b/mdx/remark.mjs @@ -0,0 +1,4 @@ +import { mdxAnnotations } from "mdx-annotations"; +import remarkGfm from "remark-gfm"; + +export const remarkPlugins = [mdxAnnotations.remark, remarkGfm]; diff --git a/mdx/search.mjs b/mdx/search.mjs new file mode 100644 index 0000000..7dca834 --- /dev/null +++ b/mdx/search.mjs @@ -0,0 +1,141 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as url from "node:url"; +import { slugifyWithCounter } from "@sindresorhus/slugify"; +import glob from "fast-glob"; +import { toString as mdastToString } from "mdast-util-to-string"; +import { remark } from "remark"; +import remarkMdx from "remark-mdx"; +import { createLoader } from "simple-functional-loader"; +import { filter } from "unist-util-filter"; +import { SKIP, visit } from "unist-util-visit"; + +const __filename = url.fileURLToPath(import.meta.url); +const processor = remark().use(remarkMdx).use(extractSections); +const slugify = slugifyWithCounter(); + +function isObjectExpression(node) { + return ( + node.type === "mdxTextExpression" && + node.data?.estree?.body?.[0]?.expression?.type === "ObjectExpression" + ); +} + +function excludeObjectExpressions(tree) { + return filter(tree, (node) => !isObjectExpression(node)); +} + +function extractSections() { + return (tree, { sections }) => { + slugify.reset(); + + visit(tree, (node) => { + if (node.type === "heading" || node.type === "paragraph") { + const content = mdastToString(excludeObjectExpressions(node)); + if (node.type === "heading" && node.depth <= 2) { + const hash = node.depth === 1 ? null : slugify(content); + sections.push([content, hash, []]); + } else { + sections.at(-1)?.[2].push(content); + } + return SKIP; + } + }); + }; +} + +export default function Search(nextConfig = {}) { + const cache = new Map(); + + return Object.assign({}, nextConfig, { + webpack(config, options) { + config.module.rules.push({ + test: __filename, + use: [ + createLoader(function () { + const appDir = path.resolve("./app"); + this.addContextDependency(appDir); + + const files = glob.sync("**/*.mdx", { cwd: appDir }); + const data = files.map((file) => { + const url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`; + const mdx = fs.readFileSync( + path.join(appDir, file), + "utf8", + ); + + let sections = []; + + if (cache.get(file)?.[0] === mdx) { + sections = cache.get(file)[1]; + } else { + const vfile = { value: mdx, sections }; + processor.runSync( + processor.parse(vfile), + vfile, + ); + cache.set(file, [mdx, sections]); + } + + return { url, sections }; + }); + + // When this file is imported within the application + // the following module is loaded: + return ` + import FlexSearch from 'flexsearch' + + let sectionIndex = new FlexSearch.Document({ + tokenize: 'full', + document: { + id: 'url', + index: 'content', + store: ['title', 'pageTitle'], + }, + context: { + resolution: 9, + depth: 2, + bidirectional: true + } + }) + + let data = ${JSON.stringify(data)} + + for (let { url, sections } of data) { + for (let [title, hash, content] of sections) { + sectionIndex.add({ + url: url + (hash ? ('#' + hash) : ''), + title, + content: [title, ...content].join('\\n'), + pageTitle: hash ? sections[0][0] : undefined, + }) + } + } + + export function search(query, options = {}) { + let result = sectionIndex.search(query, { + ...options, + enrich: true, + }) + if (result.length === 0) { + return [] + } + return result[0].result.map((item) => ({ + url: item.id, + title: item.doc.title, + pageTitle: item.doc.pageTitle, + })) + } + `; + }), + ], + }); + + if (typeof nextConfig.webpack === "function") { + return nextConfig.webpack(config, options); + } + + return config; + }, + }); +} diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..637edb1 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,21 @@ +import nextMDX from "@next/mdx"; + +import { recmaPlugins } from "./mdx/recma.mjs"; +import { rehypePlugins } from "./mdx/rehype.mjs"; +import { remarkPlugins } from "./mdx/remark.mjs"; +import withSearch from "./mdx/search.mjs"; + +const withMDX = nextMDX({ + options: { + remarkPlugins, + rehypePlugins, + recmaPlugins, + }, +}); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"], +}; + +export default withSearch(withMDX(nextConfig)); diff --git a/package.json b/package.json index 1437f94..3cbc88d 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,60 @@ { - "scripts": { - "docs:dev": "vitepress dev", - "docs:build": "vitepress build", - "docs:preview": "vitepress preview" - }, - "devDependencies": { - "@biomejs/biome": "^1.8.3", - "vitepress": "^1.3.1" - }, - "trustedDependencies": ["@biomejs/biome", "esbuild", "vue-demi"], - "dependencies": { - "@tailwindcss/vite": "^4.0.0-alpha.17", - "@vueuse/core": "^10.11.0", - "iconify-icon": "^2.1.0", - "tailwindcss": "^4.0.0-alpha.17" - } + "name": "tailwindui-protocol", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint-next": "next lint", + "lint": "bunx @biomejs/biome check ." + }, + "browserslist": "defaults, not ie <= 11", + "dependencies": { + "@algolia/autocomplete-core": "^1.7.3", + "@headlessui/react": "^2.0.1", + "@headlessui/tailwindcss": "^0.2.0", + "@mdx-js/loader": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "@next/mdx": "^14.0.4", + "@sindresorhus/slugify": "^2.1.1", + "@tailwindcss/typography": "^0.5.10", + "@types/mdx": "^2.0.8", + "@types/node": "^20.10.8", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "@types/react-highlight-words": "^0.16.4", + "acorn": "^8.8.1", + "autoprefixer": "^10.4.7", + "clsx": "^2.1.0", + "fast-glob": "^3.3.0", + "flexsearch": "^0.7.31", + "framer-motion": "^10.18.0", + "mdast-util-to-string": "^4.0.0", + "mdx-annotations": "^0.1.1", + "next": "^14.0.4", + "next-themes": "^0.2.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-highlight-words": "^0.20.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.0", + "remark-mdx": "^3.0.0", + "shiki": "^0.14.7", + "simple-functional-loader": "^1.2.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "unist-util-filter": "^5.0.1", + "unist-util-visit": "^5.0.0", + "zustand": "^4.3.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.8.3", + "eslint": "^8.56.0", + "eslint-config-next": "^14.0.4", + "prettier": "^3.1.1", + "prettier-plugin-tailwindcss": "^0.5.11", + "sharp": "0.33.1" + }, + "trustedDependencies": ["@biomejs/biome"] } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..67cdf1a --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..cc46424 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,6 @@ +/** @type {import('prettier').Options} */ +module.exports = { + singleQuote: true, + semi: false, + plugins: ["prettier-plugin-tailwindcss"], +}; diff --git a/styles/tailwind.css b/styles/tailwind.css new file mode 100644 index 0000000..6673210 --- /dev/null +++ b/styles/tailwind.css @@ -0,0 +1,21 @@ +@layer base { + :root { + --shiki-color-text: theme('colors.white'); + --shiki-token-constant: theme('colors.emerald.300'); + --shiki-token-string: theme('colors.emerald.300'); + --shiki-token-comment: theme('colors.zinc.500'); + --shiki-token-keyword: theme('colors.sky.300'); + --shiki-token-parameter: theme('colors.pink.300'); + --shiki-token-function: theme('colors.violet.300'); + --shiki-token-string-expression: theme('colors.emerald.300'); + --shiki-token-punctuation: theme('colors.zinc.200'); + } + + [inert] ::-webkit-scrollbar { + display: none; + } +} + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..4cca0b4 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,53 @@ +import headlessuiPlugin from "@headlessui/tailwindcss"; +import typographyPlugin from "@tailwindcss/typography"; +import type { Config } from "tailwindcss"; + +import typographyStyles from "./typography"; + +export default { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + darkMode: "selector", + theme: { + fontSize: { + "2xs": ["0.75rem", { lineHeight: "1.25rem" }], + xs: ["0.8125rem", { lineHeight: "1.5rem" }], + sm: ["0.875rem", { lineHeight: "1.5rem" }], + base: ["1rem", { lineHeight: "1.75rem" }], + lg: ["1.125rem", { lineHeight: "1.75rem" }], + xl: ["1.25rem", { lineHeight: "1.75rem" }], + "2xl": ["1.5rem", { lineHeight: "2rem" }], + "3xl": ["1.875rem", { lineHeight: "2.25rem" }], + "4xl": ["2.25rem", { lineHeight: "2.5rem" }], + "5xl": ["3rem", { lineHeight: "1" }], + "6xl": ["3.75rem", { lineHeight: "1" }], + "7xl": ["4.5rem", { lineHeight: "1" }], + "8xl": ["6rem", { lineHeight: "1" }], + "9xl": ["8rem", { lineHeight: "1" }], + }, + typography: typographyStyles, + extend: { + boxShadow: { + glow: "0 0 4px rgb(0 0 0 / 0.1)", + }, + maxWidth: { + lg: "33rem", + "2xl": "40rem", + "3xl": "50rem", + "5xl": "66rem", + }, + opacity: { + 1: "0.01", + // biome-ignore lint/style/useNamingConvention: + 2.5: "0.025", + // biome-ignore lint/style/useNamingConvention: + 7.5: "0.075", + 15: "0.15", + }, + }, + }, + plugins: [typographyPlugin, headlessuiPlugin], +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f62c03a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..ecb8c35 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,11 @@ +import type { SearchOptions } from "flexsearch"; + +declare module "@/mdx/search.mjs" { + export type Result = { + url: string; + title: string; + pageTitle?: string; + }; + + export function search(query: string, options?: SearchOptions): Result[]; +} diff --git a/typography.ts b/typography.ts new file mode 100644 index 0000000..1d535bc --- /dev/null +++ b/typography.ts @@ -0,0 +1,355 @@ +import type { PluginUtils } from "tailwindcss/types/config"; + +export default function typographyStyles({ theme }: PluginUtils) { + return { + DEFAULT: { + css: { + "--tw-prose-body": theme("colors.zinc.700"), + "--tw-prose-headings": theme("colors.zinc.900"), + "--tw-prose-links": theme("colors.emerald.500"), + "--tw-prose-links-hover": theme("colors.emerald.600"), + "--tw-prose-links-underline": theme("colors.emerald.500 / 0.3"), + "--tw-prose-bold": theme("colors.zinc.900"), + "--tw-prose-counters": theme("colors.zinc.500"), + "--tw-prose-bullets": theme("colors.zinc.300"), + "--tw-prose-hr": theme("colors.zinc.900 / 0.05"), + "--tw-prose-quotes": theme("colors.zinc.900"), + "--tw-prose-quote-borders": theme("colors.zinc.200"), + "--tw-prose-captions": theme("colors.zinc.500"), + "--tw-prose-code": theme("colors.zinc.900"), + "--tw-prose-code-bg": theme("colors.zinc.100"), + "--tw-prose-code-ring": theme("colors.zinc.300"), + "--tw-prose-th-borders": theme("colors.zinc.300"), + "--tw-prose-td-borders": theme("colors.zinc.200"), + + "--tw-prose-invert-body": theme("colors.zinc.400"), + "--tw-prose-invert-headings": theme("colors.white"), + "--tw-prose-invert-links": theme("colors.emerald.400"), + "--tw-prose-invert-links-hover": theme("colors.emerald.500"), + "--tw-prose-invert-links-underline": theme( + "colors.emerald.500 / 0.3", + ), + "--tw-prose-invert-bold": theme("colors.white"), + "--tw-prose-invert-counters": theme("colors.zinc.400"), + "--tw-prose-invert-bullets": theme("colors.zinc.600"), + "--tw-prose-invert-hr": theme("colors.white / 0.05"), + "--tw-prose-invert-quotes": theme("colors.zinc.100"), + "--tw-prose-invert-quote-borders": theme("colors.zinc.700"), + "--tw-prose-invert-captions": theme("colors.zinc.400"), + "--tw-prose-invert-code": theme("colors.white"), + "--tw-prose-invert-code-bg": theme("colors.zinc.700 / 0.15"), + "--tw-prose-invert-code-ring": theme("colors.white / 0.1"), + "--tw-prose-invert-th-borders": theme("colors.zinc.600"), + "--tw-prose-invert-td-borders": theme("colors.zinc.700"), + + // Base + color: "var(--tw-prose-body)", + fontSize: theme("fontSize.sm")[0], + lineHeight: theme("lineHeight.7"), + + // Text + p: { + marginTop: theme("spacing.6"), + marginBottom: theme("spacing.6"), + }, + '[class~="lead"]': { + fontSize: theme("fontSize.base")[0], + ...theme("fontSize.base")[1], + }, + + // Lists + ol: { + listStyleType: "decimal", + marginTop: theme("spacing.5"), + marginBottom: theme("spacing.5"), + paddingLeft: "1.625rem", + }, + 'ol[type="A"]': { + listStyleType: "upper-alpha", + }, + 'ol[type="a"]': { + listStyleType: "lower-alpha", + }, + 'ol[type="A" s]': { + listStyleType: "upper-alpha", + }, + 'ol[type="a" s]': { + listStyleType: "lower-alpha", + }, + 'ol[type="I"]': { + listStyleType: "upper-roman", + }, + 'ol[type="i"]': { + listStyleType: "lower-roman", + }, + 'ol[type="I" s]': { + listStyleType: "upper-roman", + }, + 'ol[type="i" s]': { + listStyleType: "lower-roman", + }, + 'ol[type="1"]': { + listStyleType: "decimal", + }, + ul: { + listStyleType: "disc", + marginTop: theme("spacing.5"), + marginBottom: theme("spacing.5"), + paddingLeft: "1.625rem", + }, + li: { + marginTop: theme("spacing.2"), + marginBottom: theme("spacing.2"), + }, + ":is(ol, ul) > li": { + paddingLeft: theme("spacing[1.5]"), + }, + "ol > li::marker": { + fontWeight: "400", + color: "var(--tw-prose-counters)", + }, + "ul > li::marker": { + color: "var(--tw-prose-bullets)", + }, + "> ul > li p": { + marginTop: theme("spacing.3"), + marginBottom: theme("spacing.3"), + }, + "> ul > li > *:first-child": { + marginTop: theme("spacing.5"), + }, + "> ul > li > *:last-child": { + marginBottom: theme("spacing.5"), + }, + "> ol > li > *:first-child": { + marginTop: theme("spacing.5"), + }, + "> ol > li > *:last-child": { + marginBottom: theme("spacing.5"), + }, + "ul ul, ul ol, ol ul, ol ol": { + marginTop: theme("spacing.3"), + marginBottom: theme("spacing.3"), + }, + + // Horizontal rules + hr: { + borderColor: "var(--tw-prose-hr)", + borderTopWidth: 1, + marginTop: theme("spacing.16"), + marginBottom: theme("spacing.16"), + maxWidth: "none", + marginLeft: `calc(-1 * ${theme("spacing.4")})`, + marginRight: `calc(-1 * ${theme("spacing.4")})`, + "@screen sm": { + marginLeft: `calc(-1 * ${theme("spacing.6")})`, + marginRight: `calc(-1 * ${theme("spacing.6")})`, + }, + "@screen lg": { + marginLeft: `calc(-1 * ${theme("spacing.8")})`, + marginRight: `calc(-1 * ${theme("spacing.8")})`, + }, + }, + + // Quotes + blockquote: { + fontWeight: "500", + fontStyle: "italic", + color: "var(--tw-prose-quotes)", + borderLeftWidth: "0.25rem", + borderLeftColor: "var(--tw-prose-quote-borders)", + quotes: '"\\201C""\\201D""\\2018""\\2019"', + marginTop: theme("spacing.8"), + marginBottom: theme("spacing.8"), + paddingLeft: theme("spacing.5"), + }, + "blockquote p:first-of-type::before": { + content: "open-quote", + }, + "blockquote p:last-of-type::after": { + content: "close-quote", + }, + + // Headings + h1: { + color: "var(--tw-prose-headings)", + fontWeight: "700", + fontSize: theme("fontSize.2xl")[0], + ...theme("fontSize.2xl")[1], + marginBottom: theme("spacing.2"), + }, + h2: { + color: "var(--tw-prose-headings)", + fontWeight: "600", + fontSize: theme("fontSize.lg")[0], + ...theme("fontSize.lg")[1], + marginTop: theme("spacing.16"), + marginBottom: theme("spacing.2"), + }, + h3: { + color: "var(--tw-prose-headings)", + fontSize: theme("fontSize.base")[0], + ...theme("fontSize.base")[1], + fontWeight: "600", + marginTop: theme("spacing.10"), + marginBottom: theme("spacing.2"), + }, + + // Media + "img, video, figure": { + marginTop: theme("spacing.8"), + marginBottom: theme("spacing.8"), + }, + "figure > *": { + marginTop: "0", + marginBottom: "0", + }, + figcaption: { + color: "var(--tw-prose-captions)", + fontSize: theme("fontSize.xs")[0], + ...theme("fontSize.xs")[1], + marginTop: theme("spacing.2"), + }, + + // Tables + table: { + width: "100%", + tableLayout: "auto", + textAlign: "left", + marginTop: theme("spacing.8"), + marginBottom: theme("spacing.8"), + lineHeight: theme("lineHeight.6"), + }, + thead: { + borderBottomWidth: "1px", + borderBottomColor: "var(--tw-prose-th-borders)", + }, + "thead th": { + color: "var(--tw-prose-headings)", + fontWeight: "600", + verticalAlign: "bottom", + paddingRight: theme("spacing.2"), + paddingBottom: theme("spacing.2"), + paddingLeft: theme("spacing.2"), + }, + "thead th:first-child": { + paddingLeft: "0", + }, + "thead th:last-child": { + paddingRight: "0", + }, + "tbody tr": { + borderBottomWidth: "1px", + borderBottomColor: "var(--tw-prose-td-borders)", + }, + "tbody tr:last-child": { + borderBottomWidth: "0", + }, + "tbody td": { + verticalAlign: "baseline", + }, + tfoot: { + borderTopWidth: "1px", + borderTopColor: "var(--tw-prose-th-borders)", + }, + "tfoot td": { + verticalAlign: "top", + }, + ":is(tbody, tfoot) td": { + paddingTop: theme("spacing.2"), + paddingRight: theme("spacing.2"), + paddingBottom: theme("spacing.2"), + paddingLeft: theme("spacing.2"), + }, + ":is(tbody, tfoot) td:first-child": { + paddingLeft: "0", + }, + ":is(tbody, tfoot) td:last-child": { + paddingRight: "0", + }, + + // Inline elements + a: { + color: "var(--tw-prose-links)", + textDecoration: "underline transparent", + fontWeight: "500", + transitionProperty: "color, text-decoration-color", + transitionDuration: theme("transitionDuration.DEFAULT"), + transitionTimingFunction: theme( + "transitionTimingFunction.DEFAULT", + ), + "&:hover": { + color: "var(--tw-prose-links-hover)", + textDecorationColor: "var(--tw-prose-links-underline)", + }, + }, + ":is(h1, h2, h3) a": { + fontWeight: "inherit", + }, + strong: { + color: "var(--tw-prose-bold)", + fontWeight: "600", + }, + ":is(a, blockquote, thead th) strong": { + color: "inherit", + }, + code: { + color: "var(--tw-prose-code)", + borderRadius: theme("borderRadius.lg"), + paddingTop: theme("padding.1"), + paddingRight: theme("padding[1.5]"), + paddingBottom: theme("padding.1"), + paddingLeft: theme("padding[1.5]"), + boxShadow: "inset 0 0 0 1px var(--tw-prose-code-ring)", + backgroundColor: "var(--tw-prose-code-bg)", + fontSize: theme("fontSize.2xs"), + }, + ":is(a, h1, h2, h3, blockquote, thead th) code": { + color: "inherit", + }, + "h2 code": { + fontSize: theme("fontSize.base")[0], + fontWeight: "inherit", + }, + "h3 code": { + fontSize: theme("fontSize.sm")[0], + fontWeight: "inherit", + }, + + // Overrides + ":is(h1, h2, h3) + *": { + marginTop: "0", + }, + "> :first-child": { + marginTop: "0 !important", + }, + "> :last-child": { + marginBottom: "0 !important", + }, + }, + }, + invert: { + css: { + "--tw-prose-body": "var(--tw-prose-invert-body)", + "--tw-prose-headings": "var(--tw-prose-invert-headings)", + "--tw-prose-links": "var(--tw-prose-invert-links)", + "--tw-prose-links-hover": "var(--tw-prose-invert-links-hover)", + "--tw-prose-links-underline": + "var(--tw-prose-invert-links-underline)", + "--tw-prose-bold": "var(--tw-prose-invert-bold)", + "--tw-prose-counters": "var(--tw-prose-invert-counters)", + "--tw-prose-bullets": "var(--tw-prose-invert-bullets)", + "--tw-prose-hr": "var(--tw-prose-invert-hr)", + "--tw-prose-quotes": "var(--tw-prose-invert-quotes)", + "--tw-prose-quote-borders": + "var(--tw-prose-invert-quote-borders)", + "--tw-prose-captions": "var(--tw-prose-invert-captions)", + "--tw-prose-code": "var(--tw-prose-invert-code)", + "--tw-prose-code-bg": "var(--tw-prose-invert-code-bg)", + "--tw-prose-code-ring": "var(--tw-prose-invert-code-ring)", + "--tw-prose-th-borders": "var(--tw-prose-invert-th-borders)", + "--tw-prose-td-borders": "var(--tw-prose-invert-td-borders)", + }, + }, + }; +} From 27631b618ea6f73045e46555a3c028367dd68a3e Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 12:12:40 +0200 Subject: [PATCH 002/110] chore: :arrow_up: Upgrade all dependencies except shiki --- bun.lockb | Bin 271604 -> 278456 bytes mdx/rehype.mjs | 2 +- package.json | 73 +++++++++++++++++++++++++------------------------ 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/bun.lockb b/bun.lockb index 90d4c9c1a348f10b0eab6b6083fc172cbfc01a4f..8ee2aa81078225eba0a9711f0563e20be0b9cc8b 100755 GIT binary patch delta 56495 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 diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs index 66999d1..a842c52 100644 --- a/mdx/rehype.mjs +++ b/mdx/rehype.mjs @@ -8,7 +8,7 @@ import { visit } from "unist-util-visit"; function rehypeParseCodeBlocks() { return (tree) => { // biome-ignore lint/style/useNamingConvention: - visit(tree, "element", (node, _, parentNode) => { + visit(tree, "element", (node, _nodeIndex, parentNode) => { if (node.tagName === "code" && node.properties.className) { parentNode.properties.language = node.properties.className[0]?.replace(/^language-/, ""); diff --git a/package.json b/package.json index 3cbc88d..9269c4c 100644 --- a/package.json +++ b/package.json @@ -6,55 +6,56 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint-next": "next lint", + "typecheck": "tsc -p .", "lint": "bunx @biomejs/biome check ." }, "browserslist": "defaults, not ie <= 11", "dependencies": { - "@algolia/autocomplete-core": "^1.7.3", - "@headlessui/react": "^2.0.1", - "@headlessui/tailwindcss": "^0.2.0", - "@mdx-js/loader": "^3.0.0", - "@mdx-js/react": "^3.0.0", - "@next/mdx": "^14.0.4", - "@sindresorhus/slugify": "^2.1.1", - "@tailwindcss/typography": "^0.5.10", - "@types/mdx": "^2.0.8", - "@types/node": "^20.10.8", - "@types/react": "^18.2.47", - "@types/react-dom": "^18.2.18", - "@types/react-highlight-words": "^0.16.4", - "acorn": "^8.8.1", - "autoprefixer": "^10.4.7", - "clsx": "^2.1.0", - "fast-glob": "^3.3.0", - "flexsearch": "^0.7.31", - "framer-motion": "^10.18.0", + "@algolia/autocomplete-core": "^1.17.4", + "@headlessui/react": "^2.1.2", + "@headlessui/tailwindcss": "^0.2.1", + "@mdx-js/loader": "^3.0.1", + "@mdx-js/react": "^3.0.1", + "@next/mdx": "^14.2.5", + "@shikijs/rehype": "^1.11.0", + "@sindresorhus/slugify": "^2.2.1", + "@tailwindcss/typography": "^0.5.13", + "@types/mdx": "^2.0.13", + "@types/node": "^20.14.11", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-highlight-words": "^0.20.0", + "acorn": "^8.12.1", + "autoprefixer": "^10.4.19", + "clsx": "^2.1.1", + "fast-glob": "^3.3.2", + "flexsearch": "^0.7.43", + "framer-motion": "^11.3.8", "mdast-util-to-string": "^4.0.0", - "mdx-annotations": "^0.1.1", - "next": "^14.0.4", - "next-themes": "^0.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "mdx-annotations": "^0.1.4", + "next": "^14.2.5", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-highlight-words": "^0.20.0", "remark": "^15.0.1", "remark-gfm": "^4.0.0", - "remark-mdx": "^3.0.0", - "shiki": "^0.14.7", + "remark-mdx": "^3.0.1", + "shiki": "0.14.7", "simple-functional-loader": "^1.2.1", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3", + "tailwindcss": "^3.4.6", + "typescript": "^5.5.3", "unist-util-filter": "^5.0.1", "unist-util-visit": "^5.0.0", - "zustand": "^4.3.2" + "zustand": "^4.5.4" }, "devDependencies": { "@biomejs/biome": "^1.8.3", - "eslint": "^8.56.0", - "eslint-config-next": "^14.0.4", - "prettier": "^3.1.1", - "prettier-plugin-tailwindcss": "^0.5.11", - "sharp": "0.33.1" + "eslint": "^9.7.0", + "eslint-config-next": "^14.2.5", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "sharp": "^0.33.4" }, - "trustedDependencies": ["@biomejs/biome"] + "trustedDependencies": ["@biomejs/biome", "sharp"] } From 02695bc22c1e4bfcd3bced4808474af5b3d2a9ad Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 12:35:55 +0200 Subject: [PATCH 003/110] feat: :sparkles: Make branding adapt to our colors --- components/Button.tsx | 6 +++--- components/Code.tsx | 6 +++--- components/Feedback.tsx | 4 ++-- components/HeroPattern.tsx | 2 +- components/Logo.tsx | 2 +- components/Navigation.tsx | 2 +- components/Resources.tsx | 6 +++--- components/Search.tsx | 6 +++--- components/Tag.tsx | 10 +++++----- components/mdx.tsx | 4 ++-- styles/tailwind.css | 6 +++--- tailwind.config.ts | 5 +++++ typography.ts | 12 ++++++------ 13 files changed, 38 insertions(+), 33 deletions(-) diff --git a/components/Button.tsx b/components/Button.tsx index 88526e8..7532a4e 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -17,13 +17,13 @@ function ArrowIcon(props: ComponentPropsWithoutRef<"svg">) { const variantStyles = { primary: - "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300", + "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-400/10 dark:text-brand-400 dark:ring-1 dark:ring-inset dark:ring-brand-400/20 dark:hover:bg-brand-400/10 dark:hover:text-brand-300 dark:hover:ring-brand-300", secondary: "rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300", - filled: "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-500 dark:text-white dark:hover:bg-emerald-400", + filled: "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-500 dark:text-white dark:hover:bg-brand-400", outline: "rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white", - text: "text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-500", + text: "text-brand-500 hover:text-brand-600 dark:text-brand-400 dark:hover:text-brand-500", }; type ButtonProps = { diff --git a/components/Code.tsx b/components/Code.tsx index 680776b..f70d73c 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -79,7 +79,7 @@ function CopyButton({ code }: { code: string }) { className={clsx( "group/button absolute right-4 top-3.5 overflow-hidden rounded-full py-1 pl-2 pr-3 text-2xs font-medium opacity-0 backdrop-blur transition focus:opacity-100 group-hover:opacity-100", copied - ? "bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20" + ? "bg-brand-400/10 ring-1 ring-inset ring-brand-400/20" : "bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5", )} onClick={() => { @@ -101,7 +101,7 @@ function CopyButton({ code }: { code: string }) { @@ -200,7 +200,7 @@ function CodeGroupHeader({ className={clsx( "border-b py-3 transition ui-not-focus-visible:outline-none", childIndex === selectedIndex - ? "border-emerald-500 text-emerald-400" + ? "border-brand-500 text-brand-400" : "border-transparent text-zinc-400 hover:text-zinc-300", )} > diff --git a/components/Feedback.tsx b/components/Feedback.tsx index 0e65c40..a8b509d 100644 --- a/components/Feedback.tsx +++ b/components/Feedback.tsx @@ -66,8 +66,8 @@ const FeedbackThanks = forwardRef>( ref={ref} className="absolute inset-0 flex justify-center md:justify-start" > -
    - +
    + Thanks for your feedback!
    diff --git a/components/HeroPattern.tsx b/components/HeroPattern.tsx index b81ff81..7f4de25 100644 --- a/components/HeroPattern.tsx +++ b/components/HeroPattern.tsx @@ -4,7 +4,7 @@ export function HeroPattern() { return (
    -
    +
    ) { return (
    +
    ); } @@ -118,7 +118,7 @@ function ResourcePattern({ />
    ) { function HighlightQuery({ text, query }: { text: string; query: string }) { return (
    @@ -321,7 +321,7 @@ const SearchInput = forwardRef< /> {autocompleteState.status === "stalled" && (
    - +
    )}
    diff --git a/components/Tag.tsx b/components/Tag.tsx index c83859a..a4b6d99 100644 --- a/components/Tag.tsx +++ b/components/Tag.tsx @@ -6,9 +6,9 @@ const variantStyles = { }; const colorStyles = { - emerald: { - small: "text-emerald-500 dark:text-emerald-400", - medium: "ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400", + brand: { + small: "text-brand-500 dark:text-brand-400", + medium: "ring-brand-300 dark:ring-brand-400/30 bg-brand-400/10 text-brand-500 dark:text-brand-400", }, sky: { small: "text-sky-500", @@ -29,7 +29,7 @@ const colorStyles = { }; const valueColorMap = { - GET: "emerald", + GET: "brand", POST: "sky", PUT: "amber", DELETE: "rose", @@ -38,7 +38,7 @@ const valueColorMap = { export function Tag({ children, variant = "medium", - color = valueColorMap[children] ?? "emerald", + color = valueColorMap[children] ?? "brand", }: { children: keyof typeof valueColorMap; variant?: keyof typeof variantStyles; diff --git a/components/mdx.tsx b/components/mdx.tsx index cdac80e..5bbd3d3 100644 --- a/components/mdx.tsx +++ b/components/mdx.tsx @@ -46,8 +46,8 @@ function InfoIcon(props: ComponentPropsWithoutRef<"svg">) { export function Note({ children }: { children: ReactNode }) { return ( -
    - +
    +
    {children}
    diff --git a/styles/tailwind.css b/styles/tailwind.css index 6673210..bfc9612 100644 --- a/styles/tailwind.css +++ b/styles/tailwind.css @@ -1,13 +1,13 @@ @layer base { :root { --shiki-color-text: theme('colors.white'); - --shiki-token-constant: theme('colors.emerald.300'); - --shiki-token-string: theme('colors.emerald.300'); + --shiki-token-constant: theme('colors.brand.300'); + --shiki-token-string: theme('colors.brand.300'); --shiki-token-comment: theme('colors.zinc.500'); --shiki-token-keyword: theme('colors.sky.300'); --shiki-token-parameter: theme('colors.pink.300'); --shiki-token-function: theme('colors.violet.300'); - --shiki-token-string-expression: theme('colors.emerald.300'); + --shiki-token-string-expression: theme('colors.brand.300'); --shiki-token-punctuation: theme('colors.zinc.200'); } diff --git a/tailwind.config.ts b/tailwind.config.ts index 4cca0b4..3a1a20a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,6 +1,7 @@ import headlessuiPlugin from "@headlessui/tailwindcss"; import typographyPlugin from "@tailwindcss/typography"; import type { Config } from "tailwindcss"; +import colors from "tailwindcss/colors"; import typographyStyles from "./typography"; @@ -33,6 +34,10 @@ export default { boxShadow: { glow: "0 0 4px rgb(0 0 0 / 0.1)", }, + colors: { + brand: colors.pink, + secondary: colors.purple, + }, maxWidth: { lg: "33rem", "2xl": "40rem", diff --git a/typography.ts b/typography.ts index 1d535bc..0df1175 100644 --- a/typography.ts +++ b/typography.ts @@ -6,9 +6,9 @@ export default function typographyStyles({ theme }: PluginUtils) { css: { "--tw-prose-body": theme("colors.zinc.700"), "--tw-prose-headings": theme("colors.zinc.900"), - "--tw-prose-links": theme("colors.emerald.500"), - "--tw-prose-links-hover": theme("colors.emerald.600"), - "--tw-prose-links-underline": theme("colors.emerald.500 / 0.3"), + "--tw-prose-links": theme("colors.brand.500"), + "--tw-prose-links-hover": theme("colors.brand.600"), + "--tw-prose-links-underline": theme("colors.brand.500 / 0.3"), "--tw-prose-bold": theme("colors.zinc.900"), "--tw-prose-counters": theme("colors.zinc.500"), "--tw-prose-bullets": theme("colors.zinc.300"), @@ -24,10 +24,10 @@ export default function typographyStyles({ theme }: PluginUtils) { "--tw-prose-invert-body": theme("colors.zinc.400"), "--tw-prose-invert-headings": theme("colors.white"), - "--tw-prose-invert-links": theme("colors.emerald.400"), - "--tw-prose-invert-links-hover": theme("colors.emerald.500"), + "--tw-prose-invert-links": theme("colors.brand.400"), + "--tw-prose-invert-links-hover": theme("colors.brand.500"), "--tw-prose-invert-links-underline": theme( - "colors.emerald.500 / 0.3", + "colors.brand.500 / 0.3", ), "--tw-prose-invert-bold": theme("colors.white"), "--tw-prose-invert-counters": theme("colors.zinc.400"), From 5ab772df2133c213415a34193013ec447a39765d Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 13:12:55 +0200 Subject: [PATCH 004/110] feat: :sparkles: Add branding, port more text from old site --- app/favicon.ico | Bin 15086 -> 0 bytes app/{quickstart => introduction}/page.mdx | 0 app/layout.tsx | 7 ++++-- app/page.mdx | 28 ++++++++++++---------- components/Logo.tsx | 26 +++++++++++--------- components/Navigation.tsx | 2 +- images/branding/logo-full.webp | Bin 0 -> 28200 bytes images/branding/logo.webp | Bin 0 -> 5434 bytes public/favicon.png | Bin 0 -> 5613 bytes 9 files changed, 37 insertions(+), 26 deletions(-) delete mode 100644 app/favicon.ico rename app/{quickstart => introduction}/page.mdx (100%) create mode 100644 images/branding/logo-full.webp create mode 100644 images/branding/logo.webp create mode 100644 public/favicon.png diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 2deafb7ba9477100832fca12b760676acd1cbb75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI3PiPcZ9LL|1nuPi%L9C(@XXziPpqQ48$*CKmLa%x#s0VGOp*<<&;4xZg4~5>9 z-isdmgIGK%p#_U5_0I|g!6ft$@z6H5v{a3WpRbu&*KuYtZ66~io?hFm`h68u*T;u?IA*KtVUzhd|e_QSHg#gWo~#qbw=2|am=!NPyV z@C3eBoxH?g>AzwSS77If%0SDXv32^d82*HFa0Cv*rxtvI?k?C0-HnVT_2ZtOL0@Mj zm5UGHEHIWO-sbEN>b1)M+J5;RKSqyN20Cpc$cg`oMc?^fw6Xt+VG_33#DIPt@~1YJ zjsjG28S}mb&uAvzWiH zMgLBY{Fg6lQ~Q6r)QSF?9QiL_tk!YS*V?_-v$M>LG;QlQ&x2|EKG37i8n~PT|K-CQ zfnPR~dV}z53uBP&K^vdLx_%6okdK=fhiu7aL%s*G(9SwI4El{uznRWKn%uR~QJW7z zHV(nMz;PzVv*l3Mx0I!% zTsP(apPW;t6JH<+nNH^4QTAg=P8rjM)IO~u3`2FF7JIoCI~d>B#bSC{T*vMT_zq6N z<}7R^wTVp+CPDYdE%=b%y_>c_3VX6Y1+p*h!w6V?M<&IXbPYRU`|_b_`;%hq%ZH}y zFG{d4AB6S?m9xr;6!zuAG<*xY;iE;^!FB!S+L1Kw!8PHF368p0WL>v_INH`|6h;VP%{T8OJaC_n@zZ_LbY7vmf09 zO&EXI|1SUB{TF@vW&Qo5=dZi}@_qkpV2rdUD()So(Q>uDAtwElf8B5)L;F9-Mx1?> zbA1o!y|K|)!}ZnX-Xz<;ef7atpm|TYje5`3mfB2=J@5@Z&A~r|UqJ8J_1aZkwV}2m z=kzS5%>NA!VMdg}1J-9|Sd*DCrVO4>53*tYqdmbI#+cSAdZ7r_b&8nX$;6=bfqf90 z!!#3nP;V3H9aMQJ(^dy{JkT>#qYpJ7 zj~eT!GU?WipPozSE$U16rRNUkLvS7DAj+o7bWJ)#LH=sZ_Xhk7KfyTv8)ckTjWkT5 F`wWs{%d-Fg diff --git a/app/quickstart/page.mdx b/app/introduction/page.mdx similarity index 100% rename from app/quickstart/page.mdx rename to app/introduction/page.mdx diff --git a/app/layout.tsx b/app/layout.tsx index e4e00db..e156118 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,8 +10,8 @@ import type { ReactNode } from "react"; export const metadata: Metadata = { title: { - template: "%s - Protocol API Reference", - default: "Protocol API Reference", + template: "%s - Lysand API Reference", + default: "Lysand API Reference", }, }; @@ -31,6 +31,9 @@ export default async function RootLayout({ return ( + + +
    diff --git a/app/page.mdx b/app/page.mdx index d3999f7..b83d3f5 100644 --- a/app/page.mdx +++ b/app/page.mdx @@ -3,40 +3,44 @@ import { Resources } from '@/components/Resources' import { HeroPattern } from '@/components/HeroPattern' export const metadata = { - title: 'API Documentation', + title: 'Lysand Documentation', description: - 'Learn everything there is to know about the Protocol API and integrate Protocol into your product.', + 'Introduction to the Lysand Protocol, a communication medium for federated applications, leveraging the HTTP stack.', } export const sections = [ + { title: 'Vocabulary', id: 'vocabulary' }, { title: 'Guides', id: 'guides' }, { title: 'Resources', id: 'resources' }, ] -# API Documentation +# Lysand Federation Protocol -Use the Protocol API to access contacts, conversations, group messages, and more and seamlessly integrate your product into the workflows of dozens of devoted Protocol users. {{ className: 'lead' }} +The Lysand Protocol is designed as a communication medium for federated applications, leveraging the HTTP stack. Its simplicity ensures ease of implementation and comprehension. {{ className: 'lead' }}
    -## Getting started {{ anchor: false }} +## Vocabulary -To get started, create a new application in your [developer settings](#), then read about how to make requests for the resources you need to access using our HTTP APIs or dedicated client SDKs. When your integration is ready to go live, publish it to our [integrations directory](#) to reach the Protocol community. {{ className: 'lead' }} + +The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used in this document as defined in [RFC 2119](https://tools.ietf.org/html/rfc2119). + -
    - -
    +The Lysand Protocol uses the following terms: +- **Entity**: A generic term for any JSON object in the protocol, such as an [Actor](./objects/actors), a [Note](./objects/notes), or a [Like](./objects/likes). Entities are uniquely identified by their `id` property. +- **Implementation**: A software application that implements the Lysand Protocol. +- **Instance**: An application deploying an **Implementation**. + - Using the same nomenclature, an ActivityPub Implementation would be `Mastodon`, and an Instance would be `mastodon.social`. +- **Federation**: The process of exchanging data between two or more **Instances**. diff --git a/components/Logo.tsx b/components/Logo.tsx index a3ee6df..ce2e6d0 100644 --- a/components/Logo.tsx +++ b/components/Logo.tsx @@ -1,16 +1,20 @@ +import logo from "@/images/branding/logo.webp"; +import clsx from "clsx"; import type { ComponentPropsWithoutRef } from "react"; -export function Logo(props: ComponentPropsWithoutRef<"svg">) { +export function Logo(props: ComponentPropsWithoutRef<"div">) { return ( - +
    + Logo + + Lysand Protocol + +
    ); } diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 10e5bc2..6faab10 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -170,7 +170,7 @@ function NavigationGroup({
  • {group.title} diff --git a/images/branding/logo-full.webp b/images/branding/logo-full.webp new file mode 100644 index 0000000000000000000000000000000000000000..8c01cc4f24ba1a576c54c6ccc0e35163b0d54228 GIT binary patch literal 28200 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 KalLU5@c#fCD;s$L literal 0 HcmV?d00001 diff --git a/images/branding/logo.webp b/images/branding/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..2132bca12a5caa39d4179ff615df0557dfad4e6e GIT binary patch literal 5434 zcmV-A6~*dONk&F86#xKNMM6+kP&il$0000G000300093006|PpNJ0t#00E$!ZIdG@ zYHUyraFWJJw^j~5aAI#<#X2MA_db_{9zB?wP zM2IjB2}yVNyRQkZ_9E8QD2(gw{&WrIQ?i5=BwXh1?&~rW>DYuDDJn!PV`FzFwbqb{ zPl7X%Tgu06)6m}?dmd9mt;^cLT^AblzGu(X;5kjiNbYcUOq)HHn`0wSO}D@;&YnH6 z3WTJRL%In-l|W7_C1bXCXOwMzEHa6kn4DF&$#hC_n}E685n-SP>r@;=aQu0%q?V(( zGjpFvPpN85HygT4vBoFq^SD{&K~NV0`H+4{FSmlAY0PPaeb^|Wzguo*_j-YVRe)Vs z%|MVXW^Z?LEhwXrT7v=fjP9c&35}*A$ApR8-U~s7VI!B0@!X_irIQiPGO2t01v37`A?7^U3@km2;wI`$c6xmZ zqW5&y=!n&Zz!#9n>F=(s2GO8I!pj`}-Bqa|m-YWp$Z9Z%Tbh$V0zyUU?)L3ERoP)! zcd(suHmek?&orgm`Ma*6-4Gh!RX+t#=5@<*pB=K|$=nEHBC8CEjP!T+yKq^mZ=V|^ zxtZL(;qif^xaoTT>+g1jfUc4^#myR@YAutxk;OwAmHFI+Qjm6`lyS%f{oS@33sB`A z&P{P}4(~^_(gV2>r1RPq^mli;ug~nZuhr=@1NytgV1oK^epR?J-5ZD4MBtnSK|3Q1 z=f)#QatvCy9mh~9QLt`+TQE2zB5~Y&$keQdzpdC^3)}!zkAuKU0UJcmbfYIq($h- z++?yl^nDQbeof)kHONP&!krFAxtp+%6Nua7Te<0V*~-d*U^M%?&tu}(Gz7W4+)(R| zbqu@jd_oQWbx{>a#Q1$IwA*oZ$ph6#*L&$ zDA`A6R%7LcVmD2GfV9Rrxly^54*MQp;AWMx)(s~?=+l^C++q%;I2M?x8#F~Sm0NV>6uUQi z{&7GyCpR^2se=v2GN|48Wc6;%B5-~aayJyaWel%grX3yK$`>d(&7OF-_{}c05){3k zz>OK4ib}S2vp!IN53D~O+?lN0p8w&#rH7I*?orNX*-&%W8#0MH0Wfl8^Z(gRnpO|Z4z|5FuJ?(zIk*uAw2a(8#gzRR806> zqC=_XDUA0kSDTH7-5shY9C{+({P~zMLPs~Wx%WlNR^=AdCFyRvHwcBs^_iB~oe4?O zgvEfA)HE$|c3X^VfP1^#;^7$84VD`LFE=IbhI2x#LGcQyJ0HA$PNHGU`{?Q>5)QLl zGhs=_y|r-{m7D5f(0<{Q&xd385jU`rdzpzu?3z01qNAL2PdBD*PjGzQ6uPB++^%%= z?#<2}dU}GzSTOrfcSs}$3p}TZa4R&1Zbls?9JeO3UPzevmdB!#-Uk~u#385)AY+>S zD+TUMmHXc(+PNci&c~2IJk=1dZdzHltS^@CMTMj6M5>xvmfX_Gr>4LybaIO>Gua`S z5S^~qD?siJ2chq5V3C));Ak47@?qmHETKFoyVnZ@Nb5@A;}W4)Zi{vzb=ypj*5b?o zFL!Vj)%l4_tUn!kH@O#X(i63PC# zZ!q9kQTMS3V$y3-6vjXKbe@%C-bJ%4+O0c^y-0oW82wSR4y?MKro!X@p9t_>`y80ZMSY4RgaeJaLJMvbv{|}N$ z5f~znVDxA*#a*C-`w)Z#3dszqqsdRb+sp-`F)*sNxf?-B)O8TCH>rDbcgH$LDI4XW zWQdL2V0s;s&qV41Z0432@}{EF&EZzC$)}@cb@z8m9g%~)x1q~x7&nq0k%_iu9i3TC zf4BWJAJ!q(xQ1?}Ly;_#Q&IzFal=Uv3L7Ke{KL4#9I7rH)|p`nw}!z!3AM8onVi|& z)ec#Sn8Phnl269@5pAYH++?Fj*n%EU<|YyweXH1|Pty9kQ-lb$rVbR`@cO%DE{TYu z&EMX=+FB9^IS2($l?QQe%d-TDCbwo6(%-EL>2wcgnWDX0>R=JBumh6nOm zOyIWaErK$zjsR1-B_XN0Aey_q92C`#QQVP8#A=~V7sxc1n^<0nh(ue_jM5ZtVkrwQ z8bRfzaYtR6+29Z}4`o<4f>h)JvWt{lvLm^xJ40GCyPMD{FXT+_>;L(qP0YfHl!0Y% zG{tT1-magHhrj{P?|IgxYwq5@-lbAIuu+tyyZhbi>uD(LnhcWYz1`nlJ^h$zlB$gL zwd(Eu@c7=_@0zZvvQe}dI=g?oyj<@8Aw5J625Ygi`>#ir%jMboVB2`cy`%89=-bS+wrHlD%L(9R)y^6zJ7dr|8nwp{Oa!OHBR6_ zJ@E>;pdFe{?(3&_ub6DP3+?%_4M8@*CcI_dl7Bp5C!v6;6#Qr*2&u|L2}D z3w+Tt6Uwiju@JfR@DOyaEKUV~djAwLG3RofxuRh*1g%6H*0&8vv;kD2 zB9sBT(WF>EHaI>wRhPpk5Ut1|QqM3^4-9$E2LV>j?=fQBP3SE0P?y3dy!oj`2TVrD zy{td&?LX~4Y*{jQdtmy?RkLdW8HvzNH&fB^m}TLBn7 zK67y%Y1>E`Cw4DK|I1Hh;?OxCA`g5sxq1FhKhaQ4E?~m`k}qA+ai ztZsyOPA1lKDKTncO=C)&pWd>b0A<5u4^&tXXX%Tn^#YAuSy(Kii5z3Ow7^gcjEDG! z$(HM~0*XlHQ;oV9m;v<{7aQ{JHu<#c;10Kj$l*c7^mHuO`Ck9GfF=$ChH@auJ)iK% z6oAL{K@}GIUh3C?n%5?XkYcz8?B$30CjAhHCBAnpf?zaIV63U~3_AZz+!H5_uQez@ zKGPdm*l>ME%Xlj1ymU6!Bg6r<3=V140vK@ogQ5S71n3`SysGt*AIGHQCSC5RHSUJT_d8eVp zIxPjYyU2z_U(<1Kgs<<*;*5YIN-UK8v=bWXVSy+@gWI4Uplp^l^}F4WZXY2i>uPkL=| z1O0E!`oR{*UwraDc7L8n(Hp9!>>b;UG>F=|7~{G$-UE>C*v7c?@<wO?!$WWGJ|C*}4QUI_ypMhwV8>X6zg+PS1ezJp-Auaw-x;5%+e z?&HCa`~+Usi3916VLI~niCenCqe5}eS=?Ea<;Ji;PdK@2KD!4OUV84i%gG6jDJW9C zIe>=mt5Ww$e})+o5kLs0X#r}?A-2$htp$NDUel3b)r87xfDEJiEp+D|(l3hmj6SGV znorrXB?rTJf@{wQ3sv&ig32thDs&XmE$F7UzeD!})=D>R>leUrow=F64zbuJN$sS; zo+EdO6u{{`wR&?8Kd9(Sjt*}qJb;oSfiUqNaA@WAM-dCLBN){c!z4<8gSGPxwX(10 zN1j<5rv*^PHa`dpQzV$lYM3*$cohI~{Qlb=E-)#K2w$U-3Ey{M(#mHLz<6EgO16_i zCO@Yva%)ClK2p)x|~as0(=6l?H=*$S~(? z(>ahAVxDi*#((@7?32JKP!Ly997@u*0X#QVt`Obcy5$9QAr`=wF9sIlo?=#gOLyU! z!A)&@Mi>w)#AzbZhF};vE$GA27qjW}+)P_cW$wxO@Ok`PZaMxm5c0%x+t%K}EG`vM zsI@i4C1;Q-J4Bmepy>X;lh}r6nU zoRh3unBpN+5%_neGq}a@8~y)Lf2RvRV4`)R==1>?!J>p!UDv59Su&M++xiOTs`Q$v03Ouy^uEU^JN;M@NI+^7Q+Oa?XO16rF-M5%+pZpki z+I-UGgT58j>-$#J|Aezeyzm`zSyh(KM?vGYi8~SM;;Zl|aYSOHom^JZUj?gS5wa5s zQB)cbknqu|L9*dgCFtLb(`8mf9!^n~m_N8$;yS#jKM}}=lG7xVe4zKIQuY$5PMjU7No93p{C66Tk4TmxbXVI6>c`Ep z#_y6k6wA2sbn1o^3>1lPJw~p0kifTgR4SwI)S`=d>53B64JMvzp$>}cx+I-?Q3MN9 z5EZStMHtIr*D`B3r?o2QgPl9on2~C=oW~3Zx$LXixO56_rrObRpu`r&{HN0f=taAY zdNE{>#Akfjoz0rc3azp5kM*XjKZ#yNZS}m|M`2sikx5DwzfoZw+#WY2LvRCIB6Vf; zW57<25UwW4lM_`=?~KYm8^DnpDh+>Q;$oEcSgIv?~xj zSf=U4neY&9HQDr(;XV+fwB~xc2ilqe`MvJ8SSOq%Wdgq&{(TTRpL}@L-+0|7wF?FO zYw>}^boN$v0Yyv^ie(l zX~NEeoid*6@o+BVmp1}@2XRdC5gjZ5QdR&P`hVau{|8xK48rTM5T@B*&%+)P<(|9V z#A2z&tZ<8=CIrQD&{Q4xx{9?w>L6+M&G&t!^2)IJ%xwU_P8hJ ze!@^(-ElP4Vj%@+!3K_6SFy-}Gw7V+QkBwWt+l2s6FBb*rMDxN+z=oumojbHazN$G z-q~yNkXIiFcyPg0gNj4mmv!dSH%+`@R_E)3Xv<0bn2K6T4_Mj&XH#@IgUUWBVS=6e z1tBnJm;zrw4vb1~!;LEdm}x>v#W0-3R0p+F#D;I2J zo>Yky6Sg93*Al`I^=QzjqK=1@bj4~#%;aqt$FU5M=wv{15NIm)mYAtZD8!+QEx`E! kjp4MBF}mSa(RDFOj$BIWnZdapbA4V6oZ+x)>I+Oj0L_5Dk^lez literal 0 HcmV?d00001 diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5bfb829e422d0e3caed8f6889a4b28408ee29c5f GIT binary patch literal 5613 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 z z!+1u=e==_AXNC*ge5rjb{OK3`8azeizx4Y`Db&fJ&mxKC=9Cv&C~9ly&tU1+nH@oX zm8Q3FY&4-69`b>)Np*mm+gR|o0zgZmjayP)xw-G};4r3!{fpGFSGA5&{q(pE<2Uo{ zMw%Zwts&-K3STjy^u6F~r_JC7XS2n^l!Yf8>R4Lq!RK425hADUSJ3afuszQ)J1zx4nGs5WC|ocraPKH~A-~A<4w;LaNNEw$d^9r9wKj*d3uQ+b_(W!3A*@b$UPmpw=Nv;;kq(g{YR; z9`GPVe6Vhh9fo{Q_aj@Q+-0xANitxl7?Tps6x)u+^`2iP zI?mC?fkqY5iy3f-SCC()g$v8Xme$15Yqr{Dy_#pf^=7tIL;mdlO$J@-9imhX`sQA{ zZq~;!*v&C$NO0J7k1dH?#!kC*Y<^YQ=VSMe7vibH<)OK?v=2@I{BNfNy>zi#oET?L z%z;R<@n`()^3LNLN8My82yBI50nRwiK)FtL|>>)pR z$Uc?*c3LF6cB_H={Pb8FLQw%9hY_(;Nyx2#+PHt{rM=(ONfN_>g!ELPb<^Ic@wqCe zHynKaK4$%-mSjHLp^H31$gZZzQuIChx|>rEw7qFO4sUzd(UJz+t!7=EnW`%N%lFL2 z4QA=qZmy+ge>&KxZ6tkZWD8e|c$FRcFSCU$Dtcx+nbou%zFIa`5;Pu0ub1%8Sa$URW6 zUk}9GLp>-)dY32m^X?cOiEX_VVrUX>r}aojPc>hK!v99Jud&BXu1zm39+iDMm>{;T zhQ5h-+qSyd@??&f5FomI9UoyJz8u#uNAO5@x=5U|v3~2G8*yVcn?bAjSLMl8{CS>C z$I=f0P1L_msc=)5aBobwk2W^chf077Tt!t8uBwQDJHX-ERHz~3|KH+a- literal 0 HcmV?d00001 From 3545bf525fdaf328f2349a07bba7ef346a6256f6 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 13:26:09 +0200 Subject: [PATCH 005/110] docs: :memo: Finish main page, add guides --- app/page.mdx | 22 +++++++++++++------- components/Guides.tsx | 17 ---------------- components/Resources.tsx | 43 ++++++++-------------------------------- 3 files changed, 23 insertions(+), 59 deletions(-) diff --git a/app/page.mdx b/app/page.mdx index b83d3f5..6b5ff98 100644 --- a/app/page.mdx +++ b/app/page.mdx @@ -3,15 +3,14 @@ import { Resources } from '@/components/Resources' import { HeroPattern } from '@/components/HeroPattern' export const metadata = { - title: 'Lysand Documentation', - description: - 'Introduction to the Lysand Protocol, a communication medium for federated applications, leveraging the HTTP stack.', + title: 'Lysand Documentation', + description: 'Introduction to the Lysand Protocol, a communication medium for federated applications, leveraging the HTTP stack.', } export const sections = [ - { title: 'Vocabulary', id: 'vocabulary' }, - { title: 'Guides', id: 'guides' }, - { title: 'Resources', id: 'resources' }, + { title: 'Vocabulary', id: 'vocabulary' }, + { title: 'Basic Concepts', id: 'basic-concepts' }, + { title: 'Resources', id: 'resources' }, ] @@ -42,6 +41,15 @@ The Lysand Protocol uses the following terms: - Using the same nomenclature, an ActivityPub Implementation would be `Mastodon`, and an Instance would be `mastodon.social`. - **Federation**: The process of exchanging data between two or more **Instances**. - +## Philosophy + +The Lysand Protocol is heavily inspired by the [ActivityPub](https://www.w3.org/TR/activitypub/) specification. It is designed to be simple and easy to implement, with a focus on the following concepts: +- **Simple Structures**: Entities are represented as JSON objects. No JSON-LD, no complex data structures, just plain JSON. +- **Modularity**: The protocol is divided into a **core protocol** and **extensions**. Implementations can choose to support only the core protocol or add extensions as needed. +- **Namespacing**: To avoid extension conflicts, all extensions are namespaced. +- **Signatures**: Most types of interactions **must** be signed with a private key. Unlike other protocols, signatures are **mandatory**, not optional. +- **Developer-Friendliness**: Understanding and implementing your own Lysand server should be easy. Documentation is aimed at developers first. + +{/* */} diff --git a/components/Guides.tsx b/components/Guides.tsx index 42fac2e..c4bb00f 100644 --- a/components/Guides.tsx +++ b/components/Guides.tsx @@ -7,23 +7,6 @@ const guides = [ name: "Authentication", description: "Learn how to authenticate your API requests.", }, - { - href: "/pagination", - name: "Pagination", - description: "Understand how to work with paginated responses.", - }, - { - href: "/errors", - name: "Errors", - description: - "Read about the different types of errors returned by the API.", - }, - { - href: "/webhooks", - name: "Webhooks", - description: - "Learn how to programmatically configure webhooks for your app.", - }, ]; export function Guides() { diff --git a/components/Resources.tsx b/components/Resources.tsx index b238d59..f5b1793 100644 --- a/components/Resources.tsx +++ b/components/Resources.tsx @@ -16,8 +16,6 @@ import type { import { GridPattern } from "./GridPattern"; import { Heading } from "./Heading"; import { ChatBubbleIcon } from "./icons/ChatBubbleIcon"; -import { EnvelopeIcon } from "./icons/EnvelopeIcon"; -import { UserIcon } from "./icons/UserIcon"; import { UsersIcon } from "./icons/UsersIcon"; interface Resource { @@ -33,11 +31,11 @@ interface Resource { const resources: Resource[] = [ { - href: "/contacts", - name: "Contacts", + href: "/entities", + name: "Entities", description: - "Learn about the contact model and how to create, retrieve, update, delete, and list contacts.", - icon: UserIcon, + "Learn how Entities work and how to use them to transmit federated data.", + icon: ChatBubbleIcon, pattern: { y: 16, squares: [ @@ -47,11 +45,11 @@ const resources: Resource[] = [ }, }, { - href: "/conversations", - name: "Conversations", + href: "/security", + name: "Security", description: - "Learn about the conversation model and how to create, retrieve, update, delete, and list conversations.", - icon: ChatBubbleIcon, + "Learn how to secure your Lysand implementation and protect your users' data.", + icon: UsersIcon, pattern: { y: -6, squares: [ @@ -60,31 +58,6 @@ const resources: Resource[] = [ ], }, }, - { - href: "/messages", - name: "Messages", - description: - "Learn about the message model and how to create, retrieve, update, delete, and list messages.", - icon: EnvelopeIcon, - pattern: { - y: 32, - squares: [ - [0, 2], - [1, 4], - ], - }, - }, - { - href: "/groups", - name: "Groups", - description: - "Learn about the group model and how to create, retrieve, update, delete, and list groups.", - icon: UsersIcon, - pattern: { - y: 22, - squares: [[0, 1]], - }, - }, ]; function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) { From 47848c33666258d22a9a4e1322f872779c008090 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 13:34:23 +0200 Subject: [PATCH 006/110] fix: :lipstick: Slightly increase padding-top for code elements --- typography.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typography.ts b/typography.ts index 0df1175..505c2e8 100644 --- a/typography.ts +++ b/typography.ts @@ -296,7 +296,7 @@ export default function typographyStyles({ theme }: PluginUtils) { code: { color: "var(--tw-prose-code)", borderRadius: theme("borderRadius.lg"), - paddingTop: theme("padding.1"), + paddingTop: theme("padding[1.5]"), paddingRight: theme("padding[1.5]"), paddingBottom: theme("padding.1"), paddingLeft: theme("padding[1.5]"), From 4d08b7e8b0695c1fb7ad07216357e89a4bf17382 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 13:41:51 +0200 Subject: [PATCH 007/110] feat: :building_construction: Export as static site --- next.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.mjs b/next.config.mjs index 637edb1..e7acbf1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,6 +16,7 @@ const withMDX = nextMDX({ /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"], + output: "export" }; export default withSearch(withMDX(nextConfig)); From 0d18130dcfbc68cf7bedfd9589222905890f12f9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 13:53:03 +0200 Subject: [PATCH 008/110] ci: :construction_worker: Add CI tests --- .github/dependabot.yml | 11 +++++ .github/workflows/check.yml | 31 ++++++++++++++ .github/workflows/codeql.yml | 79 ++++++++++++++++++++++++++++++++++++ .github/workflows/lint.yml | 33 +++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3a3cce5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..d67f299 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,31 @@ +name: Check Types + +on: + push: + branches: ['*'] + pull_request: + # The branches below must be a subset of the branches above + branches: ['main'] + +jobs: + tests: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install NPM packages + run: | + bun install + + - name: Run typechecks + run: | + bun run typecheck diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e79174b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,79 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..06c83da --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,33 @@ +name: Lint & Format + +on: + push: + branches: ["*"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + +jobs: + tests: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install NPM packages + run: | + bun install + + - name: Run linting + run: | + bunx @biomejs/biome ci . From e53dc0d9ff7dfd620609aff519dfc190deb26f87 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 14:24:03 +0200 Subject: [PATCH 009/110] docs: :lipstick: Add more info on SDKs, change border radius to semi-rounded --- app/sdks/page.mdx | 6 ++--- components/Button.tsx | 8 +++--- components/Code.tsx | 6 ++--- components/Feedback.tsx | 4 +-- components/Heading.tsx | 2 +- components/Libraries.tsx | 52 ++++++++----------------------------- components/Resources.tsx | 16 ++++++------ components/Search.tsx | 2 +- components/mdx.tsx | 2 +- images/logos/typescript.svg | 6 +++++ next.config.mjs | 2 +- 11 files changed, 41 insertions(+), 65 deletions(-) create mode 100644 images/logos/typescript.svg diff --git a/app/sdks/page.mdx b/app/sdks/page.mdx index ec7acd1..9ab9744 100644 --- a/app/sdks/page.mdx +++ b/app/sdks/page.mdx @@ -1,9 +1,9 @@ import { Libraries } from '@/components/Libraries' export const metadata = { - title: 'Protocol SDKs', + title: 'Lysand SDKs', description: - 'Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API.', + 'Lysand offers well-written SDKs in various languages to help you create Lysand applications with ease.', } export const sections = [ @@ -12,6 +12,6 @@ export const sections = [ # Protocol SDKs -The recommended way to interact with the Protocol API is by using one of our official SDKs. Today, Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API. {{ className: 'lead' }} +The Lysand development team offers a well-written SDK in TypeScript to help you create Lysand applications with ease. {{ className: 'lead' }} diff --git a/components/Button.tsx b/components/Button.tsx index 7532a4e..c8cfc5a 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -17,12 +17,12 @@ function ArrowIcon(props: ComponentPropsWithoutRef<"svg">) { const variantStyles = { primary: - "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-400/10 dark:text-brand-400 dark:ring-1 dark:ring-inset dark:ring-brand-400/20 dark:hover:bg-brand-400/10 dark:hover:text-brand-300 dark:hover:ring-brand-300", + "rounded-md bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-400/10 dark:text-brand-400 dark:ring-1 dark:ring-inset dark:ring-brand-400/20 dark:hover:bg-brand-400/10 dark:hover:text-brand-300 dark:hover:ring-brand-300", secondary: - "rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300", - filled: "rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-500 dark:text-white dark:hover:bg-brand-400", + "rounded-md bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300", + filled: "rounded-md bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-brand-500 dark:text-white dark:hover:bg-brand-400", outline: - "rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white", + "rounded-md py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white", text: "text-brand-500 hover:text-brand-600 dark:text-brand-400 dark:hover:text-brand-500", }; diff --git a/components/Code.tsx b/components/Code.tsx index f70d73c..d11abe2 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -77,7 +77,7 @@ function CopyButton({ code }: { code: string }) {
  • diff --git a/components/Resources.tsx b/components/Resources.tsx index f5b1793..9d01a8c 100644 --- a/components/Resources.tsx +++ b/components/Resources.tsx @@ -62,7 +62,7 @@ const resources: Resource[] = [ function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) { return ( -
    +
    ); @@ -81,7 +81,7 @@ function ResourcePattern({ return (
    -
    +
    -
    -
    +
    +

    - + {resource.name}

    diff --git a/components/Search.tsx b/components/Search.tsx index 89cf479..364848c 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -473,7 +473,7 @@ export function Search() {
    -
    diff --git a/app/contacts/page.mdx b/app/contacts/page.mdx deleted file mode 100644 index b75afa9..0000000 --- a/app/contacts/page.mdx +++ /dev/null @@ -1,394 +0,0 @@ -export const metadata = { - title: 'Contacts', - description: - 'On this page, we’ll dive into the different contact endpoints you can use to manage contacts programmatically.', -} - -# Contacts - -As the name suggests, contacts are a core part of Protocol — the very reason Protocol exists is so you can have secure conversations with your contacts. On this page, we'll dive into the different contact endpoints you can use to manage contacts programmatically. We'll look at how to query, create, update, and delete contacts. {{ className: 'lead' }} - -## The contact model - -The contact model contains all the information about your contacts, such as their username, avatar, and phone number. It also contains a reference to the conversation between you and the contact and information about when they were last active on Protocol. - -### Properties - - - - Unique identifier for the contact. - - - The username for the contact. - - - The phone number for the contact. - - - The avatar image URL for the contact. - - - The contact display name in the contact list. By default, this is just the - username. - - - Unique identifier for the conversation associated with the contact. - - - Timestamp of when the contact was last active on the platform. - - - Timestamp of when the contact was created. - - - ---- - -## List all contacts {{ tag: 'GET', label: '/v1/contacts' }} - - - - - This endpoint allows you to retrieve a paginated list of all your contacts. By default, a maximum of ten contacts are shown per page. - - ### Optional attributes - - - - Limit the number of contacts returned. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -G https://api.protocol.chat/v1/contacts \ - -H "Authorization: Bearer {token}" \ - -d active=true \ - -d limit=10 - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.contacts.list() - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.contacts.list() - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->contacts->list(); - ``` - - - - ```json {{ title: 'Response' }} - { - "has_more": false, - "data": [ - { - "id": "WAz8eIbvDR60rouK", - "username": "FrankMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", - "display_name": null, - "conversation_id": "xgQQXg3hrtjh7AvZ", - "last_active_at": 705103200, - "created_at": 692233200 - }, - { - "id": "hSIhXBhNe8X1d8Et" - // ... - } - ] - } - ``` - - - - ---- - -## Create a contact {{ tag: 'POST', label: '/v1/contacts' }} - - - - - This endpoint allows you to add a new contact to your contact list in Protocol. To add a contact, you must provide their Protocol username and phone number. - - ### Required attributes - - - - The username for the contact. - - - The phone number for the contact. - - - - ### Optional attributes - - - - The avatar image URL for the contact. - - - The contact display name in the contact list. By default, this is just the username. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/contacts \ - -H "Authorization: Bearer {token}" \ - -d username="FrankMcCallister" \ - -d phone_number="1-800-759-3000" \ - -d avatar_url="https://assets.protocol.chat/avatars/frank.jpg" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.contacts.create({ - username: 'FrankMcCallister', - phone_number: '1-800-759-3000', - avatar_url: 'https://assets.protocol.chat/avatars/frank.jpg', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.contacts.create( - username="FrankMcCallister", - phone_number="1-800-759-3000", - avatar_url="https://assets.protocol.chat/avatars/frank.jpg", - ) - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->contacts->create([ - 'username' => 'FrankMcCallister', - 'phone_number' => '1-800-759-3000', - 'avatar_url' => 'https://assets.protocol.chat/avatars/frank.jpg', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "WAz8eIbvDR60rouK", - "username": "FrankMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", - "display_name": null, - "conversation_id": "xgQQXg3hrtjh7AvZ", - "last_active_at": null, - "created_at": 692233200 - } - ``` - - - - ---- - -## Retrieve a contact {{ tag: 'GET', label: '/v1/contacts/:id' }} - - - - - This endpoint allows you to retrieve a contact by providing their Protocol id. Refer to [the list](#the-contact-model) at the top of this page to see which properties are included with contact objects. - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.contacts.get('WAz8eIbvDR60rouK') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.contacts.get("WAz8eIbvDR60rouK") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->contacts->get('WAz8eIbvDR60rouK'); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "WAz8eIbvDR60rouK", - "username": "FrankMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", - "display_name": null, - "conversation_id": "xgQQXg3hrtjh7AvZ", - "last_active_at": 705103200, - "created_at": 692233200 - } - ``` - - - - ---- - -## Update a contact {{ tag: 'PUT', label: '/v1/contacts/:id' }} - - - - - This endpoint allows you to perform an update on a contact. Currently, the only attribute that can be updated on contacts is the `display_name` attribute which controls how a contact appears in your contact list in Protocol. - - ### Optional attributes - - - - The contact display name in the contact list. By default, this is just the username. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -X PUT https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ - -H "Authorization: Bearer {token}" \ - -d display_name="UncleFrank" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.contacts.update('WAz8eIbvDR60rouK', { - display_name: 'UncleFrank', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.contacts.update("WAz8eIbvDR60rouK", display_name="UncleFrank") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->contacts->update('WAz8eIbvDR60rouK', [ - 'display_name' => 'UncleFrank', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "WAz8eIbvDR60rouK", - "username": "FrankMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/frank.jpg", - "display_name": "UncleFrank", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "last_active_at": 705103200, - "created_at": 692233200 - } - ``` - - - - ---- - -## Delete a contact {{ tag: 'DELETE', label: '/v1/contacts/:id' }} - - - - - This endpoint allows you to delete contacts from your contact list in Protocol. Note: This will also delete your conversation with the given contact. - - - - - - - ```bash {{ title: 'cURL' }} - curl -X DELETE https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.contacts.delete('WAz8eIbvDR60rouK') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.contacts.delete("WAz8eIbvDR60rouK") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->contacts->delete('WAz8eIbvDR60rouK'); - ``` - - - - - diff --git a/app/conversations/page.mdx b/app/conversations/page.mdx deleted file mode 100644 index 87fae52..0000000 --- a/app/conversations/page.mdx +++ /dev/null @@ -1,407 +0,0 @@ -export const metadata = { - title: 'Conversations', - description: - 'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.', -} - -# Conversations - -Conversations are an essential part of Protocol — they are the containers for the messages between you, your contacts, and groups. On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically. We'll look at how to query, create, update, and delete conversations. {{ className: 'lead' }} - -## The conversation model - -The conversation model contains all the information about the conversations between you and your contacts. In addition, conversations can also be group-based with more than one contact, they can have a pinned message, and they can be muted. - -### Properties - - - - Unique identifier for the conversation. - - - Unique identifier for the other contact in the conversation. - - - Unique identifier for the group that the conversation belongs to. - - - Unique identifier for the pinned message. - - - Whether or not the conversation has been pinned. - - - Whether or not the conversation has been muted. - - - Timestamp of when the conversation was last active. - - - Timestamp of when the conversation was last opened by the authenticated - user. - - - Timestamp of when the conversation was created. - - - Timestamp of when the conversation was archived. - - - ---- - -## List all conversations {{ tag: 'GET', label: '/v1/conversations' }} - - - - - This endpoint allows you to retrieve a paginated list of all your conversations. By default, a maximum of ten conversations are shown per page. - - ### Optional attributes - - - - Limit the number of conversations returned. - - - Only show conversations that are muted when set to `true`. - - - Only show conversations that are archived when set to `true`. - - - Only show conversations that are pinned when set to `true`. - - - Only show conversations for the specified group. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -G https://api.protocol.chat/v1/conversations \ - -H "Authorization: Bearer {token}" \ - -d limit=10 - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.conversations.list() - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.conversations.list() - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->conversations->list(); - ``` - - - - ```json {{ title: 'Response' }} - { - "has_more": false, - "data": [ - { - "id": "xgQQXg3hrtjh7AvZ", - "contact_id": "WAz8eIbvDR60rouK", - "group_id": null, - "pinned_message_id": null, - "is_pinned": false, - "is_muted": false, - "last_active_at": 705103200, - "last_opened_at": 705103200, - "created_at": 692233200, - "archived_at": null - }, - { - "id": "hSIhXBhNe8X1d8Et" - // ... - } - ] - } - ``` - - - - ---- - -## Create a conversation {{ tag: 'POST', label: '/v1/conversations' }} - - - - - This endpoint allows you to add a new conversation between you and a contact or group. A contact or group id is required to create a conversation. - - ### Required attributes - - - - Unique identifier for the other contact in the conversation. - - - Unique identifier for the group that the conversation belongs to. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/conversations \ - -H "Authorization: Bearer {token}" \ - -d 'contact_id'="WAz8eIbvDR60rouK" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.conversations.create({ - contact_id: 'WAz8eIbvDR60rouK', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.conversations.create(contact_id="WAz8eIbvDR60rouK") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->conversations->create([ - 'contact_id' => 'WAz8eIbvDR60rouK', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "xgQQXg3hrtjh7AvZ", - "contact_id": "WAz8eIbvDR60rouK", - "group_id": null, - "pinned_message_id": null, - "is_pinned": false, - "is_muted": false, - "last_active_at": null, - "last_opened_at": null, - "created_at": 692233200, - "archived_at": null - } - ``` - - - - ---- - -## Retrieve a conversation {{ tag: 'GET', label: '/v1/conversations/:id' }} - - - - - This endpoint allows you to retrieve a conversation by providing the conversation id. Refer to [the list](#the-conversation-model) at the top of this page to see which properties are included with conversation objects. - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.conversations.get('xgQQXg3hrtjh7AvZ') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.conversations.get("xgQQXg3hrtjh7AvZ") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->conversations->get('xgQQXg3hrtjh7AvZ'); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "xgQQXg3hrtjh7AvZ", - "contact_id": "WAz8eIbvDR60rouK", - "group_id": null, - "pinned_message_id": null, - "is_pinned": false, - "is_muted": false, - "last_active_at": 705103200, - "last_opened_at": 705103200, - "created_at": 692233200, - "archived_at": null - } - ``` - - - - ---- - -## Update a conversation {{ tag: 'PUT', label: '/v1/conversations/:id' }} - - - - - This endpoint allows you to perform an update on a conversation. Examples of updates are pinning a message, muting or archiving the conversation, or pinning the conversation itself. - - ### Optional attributes - - - - Unique identifier for the pinned message. - - - Whether or not the conversation has been pinned. - - - Whether or not the conversation has been muted. - - - Timestamp of when the conversation was archived. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -X PUT https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ - -H "Authorization: Bearer {token}" \ - -d 'is_muted'=true - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.conversations.update('xgQQXg3hrtjh7AvZ', { - is_muted: true, - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.conversations.update("xgQQXg3hrtjh7AvZ", is_muted=True) - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->conversations->update('xgQQXg3hrtjh7AvZ', [ - 'is_muted' => true, - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "xgQQXg3hrtjh7AvZ", - "contact_id": "WAz8eIbvDR60rouK", - "group_id": null, - "pinned_message_id": null, - "is_pinned": false, - "is_muted": true, - "last_active_at": 705103200, - "last_opened_at": 705103200, - "created_at": 692233200, - "archived_at": null - } - ``` - - - - ---- - -## Delete a conversation {{ tag: 'DELETE', label: '/v1/conversations/:id' }} - - - - - This endpoint allows you to delete your conversations in Protocol. Note: This will permanently delete the conversation and all its messages — archive it instead if you want to be able to restore it later. - - - - - - - ```bash {{ title: 'cURL' }} - curl -X DELETE https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.conversations.delete('xgQQXg3hrtjh7AvZ') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.conversations.delete("xgQQXg3hrtjh7AvZ") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->conversations->delete('xgQQXg3hrtjh7AvZ'); - ``` - - - - - diff --git a/app/errors/page.mdx b/app/errors/page.mdx deleted file mode 100644 index 15f070b..0000000 --- a/app/errors/page.mdx +++ /dev/null @@ -1,70 +0,0 @@ -export const metadata = { - title: 'Errors', - description: - 'In this guide, we will talk about what happens when something goes wrong while you work with the API.', -} - -# Errors - -In this guide, we will talk about what happens when something goes wrong while you work with the API. Mistakes happen, and mostly they will be yours, not ours. Let's look at some status codes and error types you might encounter. {{ className: 'lead' }} - -You can tell if your request was successful by checking the status code when receiving an API response. If a response comes back unsuccessful, you can use the error type and error message to figure out what has gone wrong and do some rudimentary debugging (before contacting support). - - - Before reaching out to support with an error, please be aware that 99% of all - reported errors are, in fact, user errors. Therefore, please carefully check - your code before contacting Protocol support. - - ---- - -## Status codes - -Here is a list of the different categories of status codes returned by the Protocol API. Use these to understand if a request was successful. - - - - A 2xx status code indicates a successful response. - - - A 4xx status code indicates a client error — this means it's a _you_ - problem. - - - A 5xx status code indicates a server error — you won't be seeing these. - - - ---- - -## Error types - - - - - Whenever a request is unsuccessful, the Protocol API will return an error response with an error type and message. You can use this information to understand better what has gone wrong and how to fix it. Most of the error messages are pretty helpful and actionable. - - Here is a list of the two error types supported by the Protocol API — use these to understand what you have done wrong. - - - - This means that we made an error, which is highly speculative and unlikely. - - - This means that you made an error, which is much more likely. - - - - - - - ```bash {{ title: "Error response" }} - { - "type": "api_error", - "message": "No way this is happening!?", - "documentation_url": "https://protocol.chat/docs/errors/api_error" - } - ``` - - - diff --git a/app/groups/page.mdx b/app/groups/page.mdx deleted file mode 100644 index e304787..0000000 --- a/app/groups/page.mdx +++ /dev/null @@ -1,448 +0,0 @@ -export const metadata = { - title: 'Groups', - description: - 'On this page, we’ll dive into the different group endpoints you can use to manage groups programmatically.', -} - -# Groups - -Groups are where communities live in Protocol — they are a collection of contacts you're talking to all at once. On this page, we'll dive into the different group endpoints you can use to manage groups programmatically. We'll look at how to query, create, update, and delete groups. {{ className: 'lead' }} - -## The group model - -The group model contains all the information about your groups, including what contacts are in the group and the group's name, description, and avatar. - -### Properties - - - - Unique identifier for the group. - - - The name for the group. - - - The description for the group. - - - The avatar image URL for the group. - - - Unique identifier for the conversation that belongs to the group. - - - An array of contact objects that are members of the group. - - - Timestamp of when the group was created. - - - Timestamp of when the group was archived. - - - ---- - -## List all groups {{ tag: 'GET', label: '/v1/groups' }} - - - - - This endpoint allows you to retrieve a paginated list of all your groups. By default, a maximum of ten groups are shown per page. - - ### Optional attributes - - - - Limit the number of groups returned. - - - Only show groups that are archived when set to `true`. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -G https://api.protocol.chat/v1/groups \ - -H "Authorization: Bearer {token}" \ - -d limit=10 - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.groups.list() - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.groups.list() - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->groups->list(); - ``` - - - - ```json {{ title: 'Response' }} - { - "has_more": false, - "data": [ - { - "id": "l7cGNIBKZiNJ6wqF", - "name": "Plaza Hotel", - "description": "World-renowned.", - "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", - "conversation_id": "ZYjVAbCE9g5XRlra", - "contacts": [ - { - "username": "Hector" - // ... - }, - { - "username": "Cedric" - // ... - }, - { - "username": "Hester" - // ... - }, - { - "username": "Cliff" - // ... - } - ], - "created_at": 692233200, - "archived_at": null - }, - { - "id": "hSIhXBhNe8X1d8Et" - // ... - } - ] - } - ``` - - - - ---- - -## Create a group {{ tag: 'POST', label: '/v1/groups' }} - - - - - This endpoint allows you to create a new group conversation between you and a group of your Protocol contacts. - - ### Required attributes - - - - The name for the group. - - - - ### Optional attributes - - - - The description for the group. - - - The avatar image URL for the group. - - - An array of contact objects that are members of the group. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/groups \ - -H "Authorization: Bearer {token}" \ - -d name="Plaza Hotel" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.groups.create({ - name: 'Plaza Hotel', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.groups.create(name="Plaza Hotel") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->groups->create([ - 'name' => 'Plaza Hotel', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "l7cGNIBKZiNJ6wqF", - "name": "Plaza Hotel", - "description": null, - "avatar_url": null, - "conversation_id": "ZYjVAbCE9g5XRlra", - "contacts": [], - "created_at": 692233200, - "archived_at": null - } - ``` - - - - ---- - -## Retrieve a group {{ tag: 'GET', label: '/v1/groups/:id' }} - - - - - This endpoint allows you to retrieve a group by providing the group id. Refer to [the list](#the-group-model) at the top of this page to see which properties are included with group objects. - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.groups.get('L7cGNIBKZiNJ6wqF') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.groups.get("L7cGNIBKZiNJ6wqF") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->groups->get('L7cGNIBKZiNJ6wqF'); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "l7cGNIBKZiNJ6wqF", - "name": "Plaza Hotel", - "description": "World-renowned.", - "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", - "conversation_id": "ZYjVAbCE9g5XRlra", - "contacts": [ - { - "username": "Hector" - // ... - }, - { - "username": "Cedric" - // ... - }, - { - "username": "Hester" - // ... - }, - { - "username": "Cliff" - // ... - } - ], - "created_at": 692233200, - "archived_at": null - } - ``` - - - - ---- - -## Update a group {{ tag: 'PUT', label: '/v1/groups/:id' }} - - - - - This endpoint allows you to perform an update on a group. Examples of updates are changing the name, description, and avatar or adding and removing contacts from the group. - - ### Optional attributes - - - - The new name for the group. - - - The new description for the group. - - - The new avatar image URL for the group. - - - An array of contact objects that are members of the group. - - - Timestamp of when the group was archived. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -X PUT https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ - -H "Authorization: Bearer {token}" \ - -d description="The finest in New York." - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.groups.update('L7cGNIBKZiNJ6wqF', { - description: 'The finest in New York.', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.groups.update("L7cGNIBKZiNJ6wqF", description="The finest in New York.") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->groups->update('L7cGNIBKZiNJ6wqF', [ - 'description' => 'The finest in New York.', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "l7cGNIBKZiNJ6wqF", - "name": "Plaza Hotel", - "description": "The finest in New York.", - "avatar_url": "https://assets.protocol.chat/avatars/plazahotel.jpg", - "conversation_id": "ZYjVAbCE9g5XRlra", - "contacts": [ - { - "username": "Hector" - // ... - }, - { - "username": "Cedric" - // ... - }, - { - "username": "Hester" - // ... - }, - { - "username": "Cliff" - // ... - } - ], - "created_at": 692233200, - "archived_at": null - }, - ``` - - - - ---- - -## Delete a group {{ tag: 'DELETE', label: '/v1/groups/:id' }} - - - - - This endpoint allows you to delete groups. Note: This will permanently delete the group, including the messages — archive it instead if you want to be able to restore it later. - - - - - - - ```bash {{ title: 'cURL' }} - curl -X DELETE https://api.protocol.chat/v1/groups/L7cGNIBKZiNJ6wqF \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.groups.delete('L7cGNIBKZiNJ6wqF') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.groups.delete("L7cGNIBKZiNJ6wqF") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->groups->delete('L7cGNIBKZiNJ6wqF'); - ``` - - - - - diff --git a/app/introduction/page.mdx b/app/introduction/page.mdx deleted file mode 100644 index 7bf8aea..0000000 --- a/app/introduction/page.mdx +++ /dev/null @@ -1,98 +0,0 @@ -export const metadata = { - title: 'Quickstart', - description: - 'This guide will get you all set up and ready to use the Protocol API. We’ll cover how to get started an API client and how to make your first API request.', -} - -# Quickstart - -This guide will get you all set up and ready to use the Protocol API. We'll cover how to get started using one of our API clients and how to make your first API request. We'll also look at where to go next to find all the information you need to take full advantage of our powerful REST API. {{ className: 'lead' }} - - - Before you can make requests to the Protocol API, you will need to grab your - API key from your dashboard. You find it under [Settings » API](#). - - -## Choose your client - -Before making your first API request, you need to pick which API client you will use. In addition to good ol' cURL HTTP requests, Protocol offers clients for JavaScript, Python, and PHP. In the following example, you can see how to install each client. - - - -```bash {{ title: 'cURL' }} -# cURL is most likely already installed on your machine -curl --version -``` - -```bash {{ language: 'js' }} -# Install the Protocol JavaScript SDK -npm install @example/protocol-api --save -``` - -```bash {{ language: 'python' }} -# Install the Protocol Python SDK -pip install protocol_api -``` - -```bash {{ language: 'php' }} -# Install the Protocol PHP SDK -composer require protocol/sdk -``` - - - -
    - -
    - -## Making your first API request - -After picking your preferred client, you are ready to make your first call to the Protocol API. Below, you can see how to send a GET request to the Conversations endpoint to get a list of all your conversations. In the cURL example, results are limited to ten conversations, the default page length for each client. - - - -```bash {{ title: 'cURL' }} -curl -G https://api.protocol.chat/v1/conversations \ - -H "Authorization: Bearer {token}" \ - -d limit=10 -``` - -```js -import ApiClient from '@example/protocol-api' - -const client = new ApiClient(token) - -await client.conversations.list() -``` - -```python -from protocol_api import ApiClient - -client = ApiClient(token) - -client.conversations.list() -``` - -```php -$client = new \Protocol\ApiClient($token); - -$client->conversations->list(); -``` - - - -
    - -
    - -## What's next? - -Great, you're now set up with an API client and have made your first request to the API. Here are a few links that might be handy as you venture further into the Protocol API: - -- [Grab your API key from the Protocol dashboard](#) -- [Check out the Conversations endpoint](/conversations) -- [Learn about the different error messages in Protocol](/errors) diff --git a/app/messages/page.mdx b/app/messages/page.mdx deleted file mode 100644 index e3cbca0..0000000 --- a/app/messages/page.mdx +++ /dev/null @@ -1,441 +0,0 @@ -export const metadata = { - title: 'Messages', - description: - 'On this page, we’ll dive into the different message endpoints you can use to manage messages programmatically.', -} - -# Messages - -Messages are what conversations are made of in Protocol — they are the basic building blocks of your conversations with your Protocol contacts. On this page, we'll dive into the different message endpoints you can use to manage messages programmatically. We'll look at how to query, send, update, and delete messages. {{ className: 'lead' }} - -## The message model - -The message model contains all the information about the messages and attachments you send to your contacts and groups, including how your contacts have reacted to them. - -### Properties - - - - Unique identifier for the message. - - - Unique identifier for the conversation the message belongs to. - - - The contact object for the contact who sent the message. - - - The message content. - - - An array of reaction objects associated with the message. - - - An array of attachment objects associated with the message. - - - Timestamp of when the message was read. - - - Timestamp of when the message was created. - - - Timestamp of when the message was last updated. - - - ---- - -## List all messages {{ tag: 'GET', label: '/v1/messages' }} - - - - - This endpoint allows you to retrieve a paginated list of all your messages (in a conversation if a conversation id is provided). By default, a maximum of ten messages are shown per page. - - ### Optional attributes - - - - Limit to messages from a given conversation. - - - Limit the number of messages returned. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -G https://api.protocol.chat/v1/messages \ - -H "Authorization: Bearer {token}" \ - -d conversation_id=xgQQXg3hrtjh7AvZ \ - -d limit=10 - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.messages.list() - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.messages.list() - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->messages->list(); - ``` - - - - ```json {{ title: 'Response' }} - { - "has_more": false, - "data": [ - { - "id": "SIuAFUNKdSYHZF2w", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "contact": { - "id": "WAz8eIbvDR60rouK", - "username": "KevinMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/buzzboy.jpg", - "last_active_at": 705103200, - "created_at": 692233200 - }, - "message": "It’s a nice night for a neck injury.", - "reactions": [], - "attachments": [], - "read_at": 705103200, - "created_at": 692233200, - "updated_at": 692233200 - }, - { - "id": "hSIhXBhNe8X1d8Et", - // .. - } - ] - } - ``` - - - - ---- - -## Send a message {{ tag: 'POST', label: '/v1/messages' }} - - - - - This endpoint allows you to send a new message to one of your conversations. - - ### Required attributes - - - - Unique identifier for the conversation the message belongs to. - - - The message content. - - - - ### Optional attributes - - - - An array of attachment objects associated with the message. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/messages \ - -H "Authorization: Bearer {token}" \ - -d conversation_id="xgQQXg3hrtjh7AvZ" \ - -d message="You’re what the French call ‘les incompetents.’" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.messages.send({ - conversation_id: 'xgQQXg3hrtjh7AvZ', - message: 'You’re what the French call ‘les incompetents.’', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.messages.send( - conversation_id="xgQQXg3hrtjh7AvZ", - message="You’re what the French call ‘les incompetents.’", - ) - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->messages->send([ - 'conversation_id' => 'xgQQXg3hrtjh7AvZ', - 'message' => 'You’re what the French call ‘les incompetents.’', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "gWqY86BMFRiH5o11", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "contact": { - "id": "inEIRvzjC6YLMX3o", - "username": "LinnieMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/linnie.jpg", - "last_active_at": 705103200, - "created_at": 692233200 - }, - "message": "You’re what the French call ‘les incompetents.’", - "reactions": [], - "attachments": [], - "read_at": null, - "created_at": 692233200, - "updated_at": null - } - ``` - - - - ---- - -## Retrieve a message {{ tag: 'GET', label: '/v1/messages/:id' }} - - - - - This endpoint allows you to retrieve a message by providing the message id. Refer to [the list](#the-message-model) at the top of this page to see which properties are included with message objects. - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.messages.get('SIuAFUNKdSYHZF2w') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.messages.get("SIuAFUNKdSYHZF2w") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->messages->get('SIuAFUNKdSYHZF2w'); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "SIuAFUNKdSYHZF2w", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "contact": { - "id": "WAz8eIbvDR60rouK", - "username": "KevinMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/kevin.jpg", - "last_active_at": 705103200, - "created_at": 692233200 - }, - "message": "I’m traveling with my dad. He’s at a meeting. I hate meetings.", - "reactions": [], - "attachments": [], - "read_at": 705103200, - "created_at": 692233200, - "updated_at": 692233200 - } - ``` - - - - ---- - -## Update a message {{ tag: 'PUT', label: '/v1/messages/:id' }} - - - - - This endpoint allows you to perform an update on a message. Examples of updates are adding a reaction, editing the message, or adding an attachment. - - ### Optional attributes - - - - The message content. - - - An array of reaction objects associated with the message. - - - An array of attachment objects associated with the message. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -X PUT https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ - -H "Authorization: Bearer {token}" \ - -d reactions[red_angry_face][]="KateMcCallister" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.messages.update('SIuAFUNKdSYHZF2w', { - reactions: { - red_angry_face: ['KateMcCallister'] - } - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.messages.update("SIuAFUNKdSYHZF2w", - reactions={"red_angry_face": ["KateMcCallister"]}) - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->messages->update('SIuAFUNKdSYHZF2w', [ - 'reactions' => [ - 'red_angry_face' => ['KateMcCallister'], - ], - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "SIuAFUNKdSYHZF2w", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "contact": { - "id": "WAz8eIbvDR60rouK", - "username": "KevinMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/buzzboy.jpg", - "last_active_at": 705103200, - "created_at": 692233200 - }, - "message": "I'm not apologizing. I'd rather kiss a toilet seat.", - "reactions": [ - { - "red_angry_face": [ - "KateMcCallister" - ] - } - ], - "attachments": [], - "read_at": 705103200, - "created_at": 692233200, - "updated_at": 692233200 - } - ``` - - - - ---- - -## Delete a message {{ tag: 'DELETE', label: '/v1/messages/:id' }} - - - - - This endpoint allows you to delete messages from your conversations. Note: This will permanently delete the message. - - - - - - - ```bash {{ title: 'cURL' }} - curl -X DELETE https://api.protocol.chat/v1/messages/SIuAFUNKdSYHZF2w \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.messages.delete('SIuAFUNKdSYHZF2w') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.messages.delete("SIuAFUNKdSYHZF2w") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->messages->delete('SIuAFUNKdSYHZF2w'); - ``` - - - - - diff --git a/app/pagination/page.mdx b/app/pagination/page.mdx deleted file mode 100644 index 8bec705..0000000 --- a/app/pagination/page.mdx +++ /dev/null @@ -1,63 +0,0 @@ -export const metadata = { - title: 'Pagination', - description: - 'In this guide, we will look at how to work with paginated responses when querying the Protocol API', -} - -# Pagination - -In this guide, we will look at how to work with paginated responses when querying the Protocol API. By default, all responses limit results to ten. However, you can go as high as 100 by adding a `limit` parameter to your requests. If you are using one of the official Protocol API client libraries, you don't need to worry about pagination, as it's all being taken care of behind the scenes. {{ className: 'lead' }} - -When an API response returns a list of objects, no matter the amount, pagination is supported. In paginated responses, objects are nested in a `data` attribute and have a `has_more` attribute that indicates whether you have reached the end of the last page. You can use the `starting_after` and `endding_before` query parameters to browse pages. - -## Example using cursors - - - - - In this example, we request the page that starts after the conversation with id `s4WycXedwhQrEFuM`. As a result, we get a list of three conversations and can tell by the `has_more` attribute that we have reached the end of the resultset. - - - - The last ID on the page you're currently on when you want to fetch the next page. - - - The first ID on the page you're currently on when you want to fetch the previous page. - - - Limit the number of items returned. - - - - - - - ```bash {{ title: 'Manual pagination using cURL' }} - curl -G https://api.protocol.chat/v1/conversations \ - -H "Authorization: Bearer {token}" \ - -d starting_after="s4WycXedwhQrEFuM" \ - -d limit=10 - ``` - - ```json {{ title: 'Paginated response' }} - { - "has_more": false, - "data": [ - { - "id": "WAz8eIbvDR60rouK", - // ... - }, - { - "id": "hSIhXBhNe8X1d8Et" - // ... - }, - { - "id": "fbwYwpi9C2ybt6Yb" - // ... - } - ] - } - ``` - - - diff --git a/app/webhooks/page.mdx b/app/webhooks/page.mdx deleted file mode 100644 index d8a568d..0000000 --- a/app/webhooks/page.mdx +++ /dev/null @@ -1,172 +0,0 @@ -export const metadata = { - title: 'Webhooks', - description: - 'In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol.', -} - -# Webhooks - -In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol. With webhooks, your app can know when something happens in Protocol, such as someone sending a message or adding a contact. {{ className: 'lead' }} - -## Registering webhooks - -To register a new webhook, you need to have a URL in your app that Protocol can call. You can configure a new webhook from the Protocol dashboard under [API settings](#). Give your webhook a name, pick the [events](#event-types) you want to listen for, and add your URL. - -Now, whenever something of interest happens in your app, a webhook is fired off by Protocol. In the next section, we'll look at how to consume webhooks. - -## Consuming webhooks - -When your app receives a webhook request from Protocol, check the `type` attribute to see what event caused it. The first part of the event type will tell you the payload type, e.g., a conversation, message, etc. - -```json {{ title: 'Example webhook payload' }} -{ - "id": "a056V7R7NmNRjl70", - "type": "conversation.updated", - "payload": { - "id": "WAz8eIbvDR60rouK" - // ... - } -} -``` - -In the example above, a conversation was `updated`, and the payload type is a `conversation`. - -
    - -
    - ---- - -## Event types - - - - - - - A new contact was created. - - - An existing contact was updated. - - - A contact was successfully deleted. - - - A new conversation was created. - - - An existing conversation was updated. - - - A conversation was successfully deleted. - - - A new message was created. - - - An existing message was updated. - - - A message was successfully deleted. - - - A new group was created. - - - An existing group was updated. - - - A group was successfully deleted. - - - A new attachment was created. - - - An existing attachment was updated. - - - An attachment was successfully deleted. - - - - - - - ```json {{ 'title': 'Example payload' }} - { - "id": "a056V7R7NmNRjl70", - "type": "message.updated", - "payload": { - "id": "SIuAFUNKdSYHZF2w", - "conversation_id": "xgQQXg3hrtjh7AvZ", - "contact": { - "id": "WAz8eIbvDR60rouK", - "username": "KevinMcCallister", - "phone_number": "1-800-759-3000", - "avatar_url": "https://assets.protocol.chat/avatars/kevin.jpg", - "last_active_at": 705103200, - "created_at": 692233200 - }, - "message": "I’m traveling with my dad. He’s at a meeting. I hate meetings.", - "reactions": [], - "attachments": [], - "read_at": 705103200, - "created_at": 692233200, - "updated_at": 692233200 - } - } - ``` - - - - ---- - -## Security - -To know for sure that a webhook was, in fact, sent by Protocol instead of a malicious actor, you can verify the request signature. Each webhook request contains a header named `x-protocol-signature`, and you can verify this signature by using your secret webhook key. The signature is an HMAC hash of the request payload hashed using your secret key. Here is an example of how to verify the signature in your app: - - - -```js -const signature = req.headers['x-protocol-signature'] -const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex') - -if (hash === signature) { - // Request is verified -} else { - // Request could not be verified -} -``` - -```python -from flask import request -import hashlib -import hmac - -signature = request.headers.get("x-protocol-signature") -hash = hmac.new(bytes(secret, "ascii"), bytes(payload, "ascii"), hashlib.sha256) - -if hash.hexdigest() == signature: - # Request is verified -else: - # Request could not be verified -``` - -```php -$signature = $request['headers']['x-protocol-signature']; -$hash = hash_hmac('sha256', $payload, $secret); - -if (hash_equals($hash, $signature)) { - // Request is verified -} else { - // Request could not be verified -} -``` - - - -If your generated signature matches the `x-protocol-signature` header, you can be sure that the request was truly coming from Protocol. It's essential to keep your secret webhook key safe — otherwise, you can no longer be sure that a given webhook was sent by Protocol. Don't commit your secret webhook key to GitHub! diff --git a/components/Heading.tsx b/components/Heading.tsx index fd194f5..e9f31b2 100644 --- a/components/Heading.tsx +++ b/components/Heading.tsx @@ -61,7 +61,7 @@ function Anchor({ > {inView && (
    -
    +
    From 9b75c25fa072f6da1f1356c29ae7f9ff0701b3e6 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 20:21:38 +0200 Subject: [PATCH 018/110] docs: :memo: Add ContentFormat docs --- app/entities/users/page.mdx | 16 +-- app/structures/content-format/page.mdx | 153 +++++++++++++++++++++++++ components/Navigation.tsx | 17 ++- components/mdx.tsx | 19 +++ 4 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 app/structures/content-format/page.mdx diff --git a/app/entities/users/page.mdx b/app/entities/users/page.mdx index 39b7594..16331c6 100644 --- a/app/entities/users/page.mdx +++ b/app/entities/users/page.mdx @@ -13,10 +13,10 @@ The `User` entity represents an account on a Lysand instance. Users can post [No - + The user's avatar. Must be an image format (`image/*`). - + Short description of the user. Must be text format (`text/*`). @@ -37,7 +37,7 @@ The `User` entity represents an account on a Lysand instance. Users can post [No Can only contain the following characters: `a-z` (lowercase), `0-9`, `_` and `-`. Should be limited to reasonable lengths. - + A header image for the user's profile. Also known as a cover photo or a banner. Must be an image format (`image/*`). @@ -65,19 +65,19 @@ The `User` entity represents an account on a Lysand instance. Users can post [No The user's federation outbox. Refer to the [federation documentation](/federation). - User's followers. URI must resolve to a [Collection](/structures/collections) of [User](/entities/users) entities. + User's followers. URI must resolve to a [Collection](/structures/collection) of [User](/entities/users) entities. - Users that the user follows. URI must resolve to a [Collection](/structures/collections) of [User](/entities/users) entities. + Users that the user follows. URI must resolve to a [Collection](/structures/collection) of [User](/entities/users) entities. - User's likes. URI must resolve to a [Collection](/structures/collections) of [Like](/entities/likes) entities. + User's likes. URI must resolve to a [Collection](/structures/collection) of [Like](/entities/likes) entities. - User's dislikes. URI must resolve to a [Collection](/structures/collections) of [Dislike](/entities/dislikes) entities. + User's dislikes. URI must resolve to a [Collection](/structures/collection) of [Dislike](/entities/dislikes) entities. - [Notes](/entities/notes) that the user wants to feature (also known as "pin") on their profile. URI must resolve to a [Collection](/structures/collections) of [Note](/entities/notes) entities. + [Notes](/entities/notes) that the user wants to feature (also known as "pin") on their profile. URI must resolve to a [Collection](/structures/collection) of [Note](/entities/notes) entities. diff --git a/app/structures/content-format/page.mdx b/app/structures/content-format/page.mdx new file mode 100644 index 0000000..b182f3b --- /dev/null +++ b/app/structures/content-format/page.mdx @@ -0,0 +1,153 @@ +export const metadata = { + title: 'ContentFormat', + description: 'Definition of the ContentFormat structure', +} + +# ContentFormat + +The `ContentFormat` structure is used to represent content with metadata. It supports multiple content types for the same file, such as a PNG image and a WebP image. {{ className: 'lead' }} + +A `ContentFormat` structure is defined as follows: + +```typescript +type ContentType = `${string}/${string}`; + +type ContentFormat = { + [key: ContentType]: { + ... + }; +} +``` + + +Each piece of data in the `ContentFormat` structure is meant to be a different representation of the same content. For example, a PNG image and its WebP version are different representations of the same image. Do not mix unrelated files or data in the same `ContentFormat` structure. + +**Good:** +```json +{ + "image/png": { + ... + }, + "image/webp": { + ... + } +} +``` + +**Bad:** +```json +{ + "image/png": { + ... + }, + "application/json": { + ... + } +} +``` + + +## Implementation Guidelines + +### Text + +Implementations should always process text content with the richest format available, such as HTML. However, they should also provide other formats like plain text and Markdown for compatibility with other systems. + +HTML is the recommended content type for text content, and as such every text content should have an HTML representation. If the content is not HTML, it should be converted to HTML using appropriate conversion rules. + +Rich formats include: +- `text/html` +- `text/markdown` +- `text/x.misskeymarkdown` (Misskey Flavoured Markdown, common on ActivityPub + +Clients should display the richest possible format available. If the client does not support the richest format, it should fall back to the next richest format. + +### Images + +It is a good idea to provide at least two versions of an image (if possible): one in the original format and another in a more efficient format like WebP/AVIF. This allows clients to choose the most suitable format based on their capabilities. + +## Entity Definition + + + + + + + Structure data. If `Content-Type` is a binary format, this field should be a URI to the binary data. Otherwise, it should be the content itself. Refer to the `remote` property for more information. + + + If `true`, the content is hosted remotely and should be fetched by the client. If `false`, the content is embedded in the entity. + + + A human-readable description of the content. Also known as `alt` text. + + + Size of the content in bytes. + + + Hash of the content. + + ```typescript + type HashNames = "sha256" | "sha512" | "sha3-256" | "sha3-512" | "blake2b-256" | "blake2b-512" | "blake3-256" | "blake3-512" | "md5" | "sha1" | "sha224" | "sha384" | "sha3-224" | "sha3-384" | "blake2s-256" | "blake2s-512" | "blake3-224" | "blake3-384"; + + type Hash = { + [key in HashNames]: string; + } + ``` + + + Image in [BlurHash](https://blurha.sh/) format. + + + Width of the content in pixels. Only applicable to content types that have a width. + + + Height of the content in pixels. Only applicable to content types that have a height. + + + Frames per second. Only applicable to video content. + + + Duration of the content in seconds. Only applicable to content types that have a duration. + + + + + + + ```jsonc {{ 'title': 'Images' }} + { + "image/png": { + "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png", + "remote": true, + "description": "A jolly horse running through mountains", + "size": 453933, + "hash": { + "sha256": "91714fc336210d459d4f9d9233de663be2b87ffe923f1cfd76ece9d06f7c965d" + }, + "blurhash": "UUKJ^v~q9G%L~qRj9GofRjofRjof", + "width": 1920, + "height": 1080 + } + } + ``` + + ```jsonc {{ 'title': 'Text Formats' }} + { + "text/plain": { + "content": "The consequences of today are determined by the actions of the past. To change your future, alter your decisions today.", + "remote": false + }, + "text/markdown": { + "content": "> The consequences of today are determined by the actions of the past.\n> To change your future, alter your decisions today.", + "remote": false + }, + "text/html": { + "content": "

    The consequences of today are determined by the actions of the past.

    To change your future, alter your decisions today.

    ", + "remote": false + } + } + ``` + + +
    diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 3727bb9..1b3cea3 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -255,20 +255,17 @@ export const navigation: NavGroup[] = [ { title: "Webhooks", href: "/webhooks" }, */ ], }, + { + title: "Structures", + links: [ + { title: "ContentFormat", href: "/structures/content-format" }, + { title: "Collection", href: "/structures/collection" }, + ], + }, { title: "Entities", links: [{ title: "Users", href: "/entities/users" }], }, - /* { - title: "Resources", - links: [ - { title: "Contacts", href: "/contacts" }, - { title: "Conversations", href: "/conversations" }, - { title: "Messages", href: "/messages" }, - { title: "Groups", href: "/groups" }, - { title: "Attachments", href: "/attachments" }, - ], - }, */ ]; export function Navigation(props: ComponentPropsWithoutRef<"nav">) { diff --git a/components/mdx.tsx b/components/mdx.tsx index 3dbda38..6a7cd0b 100644 --- a/components/mdx.tsx +++ b/components/mdx.tsx @@ -92,17 +92,25 @@ export function Properties({ children }: { children: ReactNode }) { ); } +const numberTypeTooltips = { + f64: "64-bit floating-point number", + i64: "64-bit signed integer", + u64: "64-bit unsigned integer", +}; + export function Property({ name, children, type, typeLink, + numberType, required, }: { name: string; children: ReactNode; type?: string; typeLink?: string; + numberType?: "f64" | "i64" | "u64"; required?: boolean; }) { return ( @@ -120,6 +128,17 @@ export function Property({ )} + {numberType && ( + <> +
    Type
    +
    + {numberType} +
    + + )} {type && ( <>
    Type
    From 76d28e04a38c46431048f1328f26bdef28df142c Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 21:50:20 +0200 Subject: [PATCH 019/110] feat: :sparkles: Add barrel roll easter egg --- components/Search.tsx | 15 +++++++++++++++ tailwind.config.ts | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/components/Search.tsx b/components/Search.tsx index 364848c..0163d39 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -301,6 +301,21 @@ const SearchInput = forwardRef< autocompleteState.status === "stalled" ? "pr-11" : "pr-4", )} {...inputProps} + onInput={(event) => { + if ( + event.currentTarget.value + .toLowerCase() + .includes("barrel roll") + ) { + event.currentTarget.value = ""; + document.body.classList.add("animate-roll"); + setTimeout( + () => + document.body.classList.remove("animate-roll"), + 2000, + ); + } + }} onKeyDown={(event) => { if ( event.key === "Escape" && diff --git a/tailwind.config.ts b/tailwind.config.ts index 3a1a20a..922e84d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -52,6 +52,15 @@ export default { 7.5: "0.075", 15: "0.15", }, + animation: { + roll: "roll 2s 1 ease-in-out", + }, + keyframes: { + roll: { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + }, }, }, plugins: [typographyPlugin, headlessuiPlugin], From 0234304198066274d4206fd1e14eeb8fc0781896 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 23 Jul 2024 00:23:42 +0200 Subject: [PATCH 020/110] docs: :memo: Add documentation for entities --- app/entities/page.mdx | 83 +++++++++++++++++++++++++++++++++++++ app/entities/users/page.mdx | 14 +++---- app/types/page.mdx | 45 ++++++++++++++++++++ components/Navigation.tsx | 6 +-- 4 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 app/types/page.mdx diff --git a/app/entities/page.mdx b/app/entities/page.mdx index e69de29..ae6d366 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -0,0 +1,83 @@ +export const metadata = { + title: 'Entities', + description: + 'Entities are simple JSON objects that represent the core data structures in Lysand.', +} + +# Entities + +Entities are the foundation of the Lysand protocol. A similar concept to entities are the [ActivityStreams](https://www.w3.org/TR/activitystreams-core/) objects, which are used to represent activities in the [ActivityPub](https://www.w3.org/TR/activitypub/) protocol. {{ className: 'lead' }} + +## Entity Definition + +An entity is a simple JSON object that represents a core data structure in Lysand. Entities are used to represent various types of data, such as users, notes, and more. Each entity has a unique `id` property that is used to identify it within the instance. + +Any field in an entity not marked as `required` may be omitted or set to `null`. + + + + + + + Unique identifier for the entity. Must be unique within the instance. + + Must be a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier). Any version of UUID is allowed. + + + Type of the entity. Only types defined in the Lysand protocol are allowed. Use an [Extension](/extensions) if you want to define custom types. + + + Date and time when the entity was created. Must be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted string. + + + Handling of dates that are valid but obviously incorrect (e.g. in the future) is left to the Implementation's discretion. + + + + URI of the entity. Must `id`. Should be unique and resolve to the entity. + + + Extensions to the entity. Use this to add custom properties to the entity. + + Each custom property must be namespaced with the organization's domain name, followed by the property name. Extensions should be used sparingly and only when necessary. + + + + + + ```jsonc {{ 'title': 'Example Entity' }} + { + "id": "9a8928b6-2526-4979-aab1-ef2f88cd5700", + "type": "Freeze", + "created_at": "2022-01-01T12:00:00Z", + "uri": "https://bongo.social/notes/9a8928b6-2526-4979-aab1-ef2f88cd5700", + } + ``` + + ```jsonc {{ 'title': 'With Extensions' }} + { + "id": "f0aacf0b-df7a-4ee5-a2ba-6c4acafd8642", + "type": "Zlorb", + "created_at": "2023-04-13T08:00:00Z", + "uri": "https://space.org/zlorbs/f0aacf0b-df7a-4ee5-a2ba-6c4acafd8642", + "extensions": { // [!code focus:100] + "org.space:zlorbs": { + "zlorb_type": "giant", + "zlorb_size": "huge" + }, + "org.lysand:location": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + } + ``` + + + +## Serialization + +When serialized to a string, the JSON representation of an entity should follow the following rules: +- Keys must be sorted lexicographically. +- Should use UTF-8 encoding. +- Must be **signed** using the relevant [User](/entities/users)'s private key, or the [Server Actor](/entities/server-actor)'s private key if the entity is not associated with a particular user. \ No newline at end of file diff --git a/app/entities/users/page.mdx b/app/entities/users/page.mdx index 16331c6..0e32b8a 100644 --- a/app/entities/users/page.mdx +++ b/app/entities/users/page.mdx @@ -58,25 +58,25 @@ The `User` entity represents an account on a Lysand instance. Users can post [No User consent to be indexed by search engines. If `false`, the user's profile should not be indexed. - + The user's federation inbox. Refer to the [federation documentation](/federation). - + The user's federation outbox. Refer to the [federation documentation](/federation). - + User's followers. URI must resolve to a [Collection](/structures/collection) of [User](/entities/users) entities. - + Users that the user follows. URI must resolve to a [Collection](/structures/collection) of [User](/entities/users) entities. - + User's likes. URI must resolve to a [Collection](/structures/collection) of [Like](/entities/likes) entities. - + User's dislikes. URI must resolve to a [Collection](/structures/collection) of [Dislike](/entities/dislikes) entities. - + [Notes](/entities/notes) that the user wants to feature (also known as "pin") on their profile. URI must resolve to a [Collection](/structures/collection) of [Note](/entities/notes) entities. diff --git a/app/types/page.mdx b/app/types/page.mdx new file mode 100644 index 0000000..bdf4524 --- /dev/null +++ b/app/types/page.mdx @@ -0,0 +1,45 @@ + +## ISO8601 + +```typescript +type Year = `${number}${number}${number}${number}`; +type Month = `${"0" | "1"}${number}`; +type Day = `${"0" | "1" | "2" | "3"}${number}`; + +type DateString = `${Year}-${Month}-${Day}`; + +type Hour = `${"0" | "1" | "2"}${number}`; +type Minute = `${"0" | "1" | "2" | "3" | "4" | "5"}${number}`; +type Second = `${"0" | "1" | "2" | "3" | "4" | "5"}${number}`; + +type TimeString = `${Hour}:${Minute}:${Second}`; + +type Offset = `${"Z" | "+" | "-"}${Hour}:${Minute}`; + +type ISO8601 = `${DateString}T${TimeString}${Offset}`; +``` + +## UUID + +```typescript +type UUID = `${number}-${number}-${number}-${number}-${number}`; +``` + +## URI + +```typescript +type URI = string; +``` + +## Extensions + +```typescript +type OrgNamespace = string; +type ExtensionName = string; + +type ExtensionsKey = `${OrgNamespace}:${ExtensionName}`; + +type Extensions = { + [key in ExtensionsKey]: any; +} +``` \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 1b3cea3..ac78159 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -247,12 +247,8 @@ export const navigation: NavGroup[] = [ title: "Guides", links: [ { title: "Introduction", href: "/" }, - /* { title: "Quickstart", href: "/quickstart" }, */ { title: "SDKs", href: "/sdks" }, - /* { title: "Authentication", href: "/authentication" }, - { title: "Pagination", href: "/pagination" }, - { title: "Errors", href: "/errors" }, - { title: "Webhooks", href: "/webhooks" }, */ + { title: "Entities", href: "/entities" }, ], }, { From 4f685d1ec6fd4f69646fd8f09c69e0fc37159d67 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 23 Jul 2024 02:25:45 +0200 Subject: [PATCH 021/110] feat: :sparkles: Add new landing page --- app/{ => introduction}/page.mdx | 0 app/page.tsx | 104 ++++++++++++++++++++++++++++++++ components/Navigation.tsx | 2 +- components/Resources.tsx | 55 ++++++++++------- 4 files changed, 137 insertions(+), 24 deletions(-) rename app/{ => introduction}/page.mdx (100%) create mode 100644 app/page.tsx diff --git a/app/page.mdx b/app/introduction/page.mdx similarity index 100% rename from app/page.mdx rename to app/introduction/page.mdx diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..1e40eca --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,104 @@ +import { Resource, type ResourceType } from "@/components/Resources"; +import { wrapper } from "@/components/mdx"; +import type { Metadata } from "next"; +import type { FC } from "react"; + +export const metadata: Metadata = { + title: "Lysand Documentation", + description: + "Introduction to the Lysand Protocol, a communication medium for federated applications, leveraging the HTTP stack.", +}; + +const Page: FC = () => { + const resources: ResourceType[] = [ + { + name: "JSON-based APIs", + description: "Simple JSON objects are used to represent all data.", + icon: "tabler:code-dots", + }, + { + name: "MIT licensed", + description: + "Lysand is licensed under the MIT License, which allows you to use it for any purpose.", + icon: "tabler:license", + }, + { + name: "Built-in namespaced extensions", + description: + "Extensions for common use cases are built-in, such as custom emojis and reactions", + icon: "tabler:puzzle", + }, + { + name: "Easy to implement", + description: + "Lysand is designed to be easy to implement in any language.", + icon: "tabler:code", + }, + { + name: "Signed by default", + description: + "All requests are signed using advanced cryptographic algorithms.", + icon: "tabler:shield-check", + }, + { + name: "No vendor lock-in", + description: + "Standardization is heavy and designed to break vendor lock-in.", + icon: "tabler:database", + }, + { + name: "In-depth security docs", + description: + "Docs provide lots of information on how to program a secure server.", + icon: "tabler:shield", + }, + { + name: "Official SDKs", + description: "Official SDKs are available for TypeScript", + icon: "tabler:plug", + }, + ]; + + return wrapper({ + children: ( + <> +
    +

    + Lysand +

    +

    + Federation, simpler +

    +

    + A simple, extensible federated protocol for building + useful applications. +

    +
    + +

    Made by developers

    + +

    + Lysand is designed and maintained by the developers of the + Lysand Server, which uses Lysand for federation. This + community could include you! Check out our{" "} + + Git repository + {" "} + to see how you can contribute. +

    + +
    + {resources.map((resource) => ( + + ))} +
    + + ), + }); +}; + +export default Page; diff --git a/components/Navigation.tsx b/components/Navigation.tsx index ac78159..82fac8a 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -246,7 +246,7 @@ export const navigation: NavGroup[] = [ { title: "Guides", links: [ - { title: "Introduction", href: "/" }, + { title: "Introduction", href: "/introduction" }, { title: "SDKs", href: "/sdks" }, { title: "Entities", href: "/entities" }, ], diff --git a/components/Resources.tsx b/components/Resources.tsx index 9d01a8c..14a38ae 100644 --- a/components/Resources.tsx +++ b/components/Resources.tsx @@ -8,34 +8,29 @@ import { } from "framer-motion"; import Link from "next/link"; -import type { - ComponentPropsWithoutRef, - ComponentType, - MouseEvent, -} from "react"; +import { Icon } from "@iconify-icon/react"; +import type { ComponentPropsWithoutRef, MouseEvent } from "react"; import { GridPattern } from "./GridPattern"; import { Heading } from "./Heading"; -import { ChatBubbleIcon } from "./icons/ChatBubbleIcon"; -import { UsersIcon } from "./icons/UsersIcon"; -interface Resource { - href: string; +export interface ResourceType { + href?: string; name: string; description: string; - icon: ComponentType<{ className?: string }>; - pattern: Omit< + icon: string; + pattern?: Omit< ComponentPropsWithoutRef, "width" | "height" | "x" >; } -const resources: Resource[] = [ +const resources: ResourceType[] = [ { href: "/entities", name: "Entities", description: "Learn how Entities work and how to use them to transmit federated data.", - icon: ChatBubbleIcon, + icon: "tabler:code-asterisk", pattern: { y: 16, squares: [ @@ -49,7 +44,7 @@ const resources: Resource[] = [ name: "Security", description: "Learn how to secure your Lysand implementation and protect your users' data.", - icon: UsersIcon, + icon: "tabler:building-bank", pattern: { y: -6, squares: [ @@ -60,10 +55,14 @@ const resources: Resource[] = [ }, ]; -function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) { +function ResourceIcon({ icon }: { icon: string }) { return (
    - +
    ); } @@ -72,7 +71,7 @@ function ResourcePattern({ mouseX, mouseY, ...gridProps -}: Resource["pattern"] & { +}: ResourceType["pattern"] & { mouseX: MotionValue; mouseY: MotionValue; }) { @@ -110,7 +109,7 @@ function ResourcePattern({ ); } -function Resource({ resource }: { resource: Resource }) { +export function Resource({ resource }: { resource: ResourceType }) { const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); @@ -131,7 +130,13 @@ function Resource({ resource }: { resource: Resource }) { className="group relative flex rounded-md bg-zinc-50 transition-shadow hover:shadow-md hover:shadow-zinc-900/5 dark:bg-white/2.5 dark:hover:shadow-black/5" > @@ -139,10 +144,14 @@ function Resource({ resource }: { resource: Resource }) {

    - - - {resource.name} - + {resource.href ? ( + + + {resource.name} + + ) : ( + resource.name + )}

    {resource.description} From be3faaade91e8a1283bf85123fdaa6d30fefafdc Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 24 Jul 2024 00:02:06 +0200 Subject: [PATCH 022/110] docs: :memo: Define user addresses --- app/entities/users/page.mdx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/entities/users/page.mdx b/app/entities/users/page.mdx index 0e32b8a..d1f9a59 100644 --- a/app/entities/users/page.mdx +++ b/app/entities/users/page.mdx @@ -7,6 +7,36 @@ export const metadata = { The `User` entity represents an account on a Lysand instance. Users can post [Notes](/entities/notes), follow other users, and interact with content. Users are identified by their `id` property, which is unique within the instance. {{ className: 'lead' }} +## Addresses + +Users may be represented by a shorthand address, in the following formats: + +``` +@username@instance +@uuid@instance +``` + +For example: + +``` +@jessew@social.lysand.org +@018ec082-0ae1-761c-b2c5-22275a611771@social.lysand.org +``` + +This is similar to an email address or an ActivityPub address. + +### Identifier + +Identifier **must** be either a valid `username` or a valid `id`. It should have the same username/id as the user's profile. + + +Usernames can be changed by the user, so it is recommended to use `id` for long-term references. + + +### Instance + +Instance **must** be the host of the instance the user is on (hostname with optional port). + ## Entity Definition From 850efb4bd36066e745e3f1e5a090edc910f9b6af Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 24 Jul 2024 14:21:49 +0200 Subject: [PATCH 023/110] chore: :arrow_up: Upgrade dependencies, add bundle analyzer --- bun.lockb | Bin 184424 -> 191096 bytes next.config.mjs | 7 +++++-- package.json | 11 ++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0a9f1c5d8b6c21d9c43c227359a79bd0374a018f..e8e7381a2fb004902480ad8a5857f1a2b628ecdd 100755 GIT binary patch delta 33544 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 LyW?NnGFJaDNMQ{4 diff --git a/next.config.mjs b/next.config.mjs index 2050c98..14c5c0d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,6 @@ import nextMDX from "@next/mdx"; +import bundleAnalyzer from "@next/bundle-analyzer"; import { recmaPlugins } from "./mdx/recma.mjs"; import { rehypePlugins } from "./mdx/rehype.mjs"; import { remarkPlugins } from "./mdx/remark.mjs"; @@ -12,11 +13,13 @@ const withMDX = nextMDX({ recmaPlugins, }, }); - /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"], output: "export", }; -export default withSearch(withMDX(nextConfig)); +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); +export default withBundleAnalyzer(withSearch(withMDX(nextConfig))); diff --git a/package.json b/package.json index 28e059b..cc50114 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@sindresorhus/slugify": "^2.2.1", "@tailwindcss/typography": "^0.5.13", "@types/mdx": "^2.0.13", - "@types/node": "^20.14.11", + "@types/node": "^20.14.12", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-highlight-words": "^0.20.0", @@ -29,7 +29,7 @@ "clsx": "^2.1.1", "fast-glob": "^3.3.2", "flexsearch": "^0.7.43", - "framer-motion": "^11.3.8", + "framer-motion": "^11.3.15", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.4", "next": "^14.2.5", @@ -40,10 +40,10 @@ "remark": "^15.0.1", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", - "shiki": "^1.11.0", + "shiki": "^1.11.1", "simple-functional-loader": "^1.2.1", "tailwindcss": "^3.4.6", - "typescript": "^5.5.3", + "typescript": "^5.5.4", "unist-util-filter": "^5.0.1", "unist-util-visit": "^5.0.0", "zustand": "^4.5.4" @@ -51,7 +51,8 @@ "devDependencies": { "@biomejs/biome": "^1.8.3", "@iconify-icon/react": "^2.1.0", - "@shikijs/transformers": "^1.11.0", + "@next/bundle-analyzer": "^14.2.5", + "@shikijs/transformers": "^1.11.1", "sharp": "^0.33.4" }, "trustedDependencies": ["@biomejs/biome", "sharp"] From 6b0e6785e21483f483cd1bcb674b734be08e9a76 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 24 Jul 2024 15:13:24 +0200 Subject: [PATCH 024/110] feat: :sparkles: Add experimental warning banner --- app/layout.tsx | 2 +- components/ExperimentalWarning.tsx | 22 ++++++++++++++++++++++ components/Layout.tsx | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 components/ExperimentalWarning.tsx diff --git a/app/layout.tsx b/app/layout.tsx index e156118..a29bffb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,7 +10,7 @@ import type { ReactNode } from "react"; export const metadata: Metadata = { title: { - template: "%s - Lysand API Reference", + template: "%s • Lysand API Reference", default: "Lysand API Reference", }, }; diff --git a/components/ExperimentalWarning.tsx b/components/ExperimentalWarning.tsx new file mode 100644 index 0000000..2ec1bda --- /dev/null +++ b/components/ExperimentalWarning.tsx @@ -0,0 +1,22 @@ +import type { FC } from "react"; + +export const ExperimentalWarning: FC = () => ( + <> +

    +
    +

    + Warning! + + This is a testing site used for development, not a finished + page. +

    +
    +
    + +); diff --git a/components/Layout.tsx b/components/Layout.tsx index b07de15..fd50fa1 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import type { ReactNode } from "react"; +import { ExperimentalWarning } from "./ExperimentalWarning"; import { Footer } from "./Footer"; import { Header } from "./Header"; import { Logo } from "./Logo"; @@ -42,6 +43,7 @@ export function Layout({
    + ); } From b321eb2c1dbec849216e8cccb38b74547f3b7ac2 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 24 Jul 2024 15:31:45 +0200 Subject: [PATCH 025/110] docs: :memo: Document the Note entity --- app/entities/notes/page.mdx | 160 +++++++++++++++++++++++++++++ components/ExperimentalWarning.tsx | 3 +- components/Navigation.tsx | 5 +- 3 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 app/entities/notes/page.mdx diff --git a/app/entities/notes/page.mdx b/app/entities/notes/page.mdx new file mode 100644 index 0000000..2eee397 --- /dev/null +++ b/app/entities/notes/page.mdx @@ -0,0 +1,160 @@ +export const metadata = { + title: 'Notes', + description: 'Definition of the Note entity', +} + +# Notes + +Notes represent a piece of content on a Lysand instance. They can be posted by [Users](/entities/users) and are displayed in a user's feed. Notes can contain text, images, and other media. {{ className: 'lead' }} + + + Notes are not just limited to microblogging. They can be used for any kind of content, such as forum posts, blog posts, image posts, video posts, audio posts, and even messaging. + + +## Entity Definition + + + + + + Media attachments to the note. May be any format. + + + URI of the `User` considered the author of the note. + + + Category of the note. Useful for clients to render notes differently depending on their intended purpose. + + ```typescript + type Category = + | "microblog" // Like Twitter, Mastodon + | "forum" // Like Reddit + | "blog" // Like WordPress, Medium + | "image" // Like Instagram + | "video" // Like YouTube, PeerTube + | "audio" // Like SoundCloud, Spotify + | "messaging"; // Like Discord, Element (Matrix), Signal + ``` + + + The content of the note. Must be text format (`text/html`, `text/markdown`, etc). Must not be remote. + + + Device used to post the note. Useful for functionality such as Twitter's "posted via" feature. + + ```typescript + type Device = { + name: string; + version?: string; + url?: string; + } + ``` + + + URI of a [Group](/groups) that the note is only visible in, or one of the following strings: + - `public`: The note is visible to anyone. + - `followers`: The note is visible only to the author's followers. + + If not provided, the note is only visible to the author and those mentioned in the note. + + + Whether the note contains "sensitive content". This can be used with `subject` as a "content warning" feature. + + + URIs of [Users](/entities/users) that should be notified of the note. Similar to Twitter's `@` mentions. The note may also contain mentions in the content, however only the mentions in this field should trigger notifications. + + + Previews for any links in the publication. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every server that sees the publication, creating an accidental DDOS attack. + + ```typescript + type LinkPreview = { + link: string; + title: string; + description?: string; + image?: string; + icon?: string; + } + ``` + + + Servers should make sure not to trust the previews, as they could be faked by malicious remote servers. This is not a very good attack vector, but it is still possible to redirect users to malicious links. + + + + URI of the note that this note is quoting, if any. Quoting is similar to replying, but does not notify the author of the quoted note. Inspired by Twitter's "quote tweet" feature. + + + URI of the note that this note is a reply to, if any. + + + A subject for the note. Useful for clients to display a "content warning" or "spoiler" feature, such as on Mastodon. Must be a plaintext string (`text/plain`). + + + + + + ```jsonc {{ 'title': 'Example Note' }} + { + "id": "01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "type": "Note", + "uri": "https://social.lysand.org/objects/01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "created_at": "2024-06-19T01:07:44.139Z", + "attachments": [ // [!code focus:100] + { + "image/png": { + "content": "https://cdn.lysand.org/29e810bf4707fef373d886af322089d5db300fce66e4e073efc26827f10825f6/image.webp", + "remote": true, + "blurhash": "UPBy,E-qM{M_~qx]RjRP^+xuRjRjx[kWjajF", + "description": "", + "height": 960, + "size": 221275, + "hash": { + "sha256": "967b8e86d8708c6283814f450efcbd3be94d3d24ca9a7ab435b2ff8b51dcbc21" + }, + "width": 639 + } + }, + { + "image/png": { + "content": "https://cdn.lysand.org/4f87598d377441e78f3c8cfa7bd7d19d61a7470bfe0abcbee6eb1de87279fb3b/image.webp", + "remote": true, + "blurhash": "U14xf0_N~XRp?btkxaIn$--r%4=~NFniNFad", + "description": "", + "height": 1051, + "size": 122577, + "hash": { + "sha256": "fe4ef842e04495dac7c148b440d83f366b948c4a18557457109ac951d5c46eaf" + }, + "width": 926 + } + } + ], + "author": "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a", + "category": "microblog", + "content": { + "text/html": { + "content": "

    In the next lysand-fe update: account settings, finally!

    " + }, + "text/plain": { + "content": "In the next lysand-fe update: account settings, finally!" + } + }, + "device": { + "name": "Megalodon for Android", + "version": "1.3.89", + "url": "https://sk22.github.io/megalodon" + }, + "extensions": { + "org.lysand:custom_emojis": { + "emojis": [] + } + }, + "group": "public", + "is_sensitive": false, + "mentions": [], + "subject": "Lysand development" + } + ``` + + +
    \ No newline at end of file diff --git a/components/ExperimentalWarning.tsx b/components/ExperimentalWarning.tsx index 2ec1bda..08bddc2 100644 --- a/components/ExperimentalWarning.tsx +++ b/components/ExperimentalWarning.tsx @@ -13,8 +13,7 @@ export const ExperimentalWarning: FC = () => ( > - This is a testing site used for development, not a finished - page. + This site is experimental and under active development.

    diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 82fac8a..4a02641 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -260,7 +260,10 @@ export const navigation: NavGroup[] = [ }, { title: "Entities", - links: [{ title: "Users", href: "/entities/users" }], + links: [ + { title: "Users", href: "/entities/users" }, + { title: "Notes", href: "/entities/notes" }, + ], }, ]; From 66a0c9465225643c8def23b194733b6b9a494ec9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 12:41:36 +0200 Subject: [PATCH 026/110] chore: :arrow_up: Upgrade dependencies --- bun.lockb | Bin 191096 -> 191096 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index e8e7381a2fb004902480ad8a5857f1a2b628ecdd..0a724af2c4f5a35f397c649423e1725322d37407 100755 GIT binary patch delta 149 zcmV;G0BZmE)C>633y>}#@)$_d?BEqoC|C)$C?~g$@G0Sz9#?1zcE|IERk{{Iu};ce zlT>jik{$@NNM0qXKrNRXlJ-mS80dFR+qvHhLCStdL(7?BB{?g;HCH~rr6J>Y(6534 zus|2?#HZ{hK|@mXeV@pwCD~N@#uXzmAGfUt0ZQTlHj5FR0XMfaq5?!n DII}_T delta 149 zcmV;G0BZmE)C>633y>}#Q$$NZJk=R8vhM?Pb4&x{EuHN}-LEZ+7fCJ$=SU@)u};ce zlN2Q=k{$@NNM0qXK-D;-*L8D-HJ_rX048axUg-t9&1eu9&kD@+fqnk-UcS<j5FR0X4TYq5?!n DdL2O# diff --git a/package.json b/package.json index cc50114..3e6266a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "clsx": "^2.1.1", "fast-glob": "^3.3.2", "flexsearch": "^0.7.43", - "framer-motion": "^11.3.15", + "framer-motion": "^11.3.17", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.4", "next": "^14.2.5", From 1759c4ffba2386d5b90ed90f0a9a058ba03b34a5 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 14:18:17 +0200 Subject: [PATCH 027/110] refactor: :lipstick: Change code block theme to Dark Plus --- components/Code.tsx | 2 +- mdx/rehype.mjs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/components/Code.tsx b/components/Code.tsx index d11abe2..2bfbef0 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -162,7 +162,7 @@ function CodePanel({
    -
    +                
                         {children}
                     
    diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs index 273b1ae..87c7c52 100644 --- a/mdx/rehype.mjs +++ b/mdx/rehype.mjs @@ -7,7 +7,7 @@ import { slugifyWithCounter } from "@sindresorhus/slugify"; import * as acorn from "acorn"; import { toString as mdastToString } from "mdast-util-to-string"; import { mdxAnnotations } from "mdx-annotations"; -import { createCssVariablesTheme, createHighlighter } from "shiki"; +import { createHighlighter } from "shiki"; import { visit } from "unist-util-visit"; function rehypeParseCodeBlocks() { @@ -22,13 +22,6 @@ function rehypeParseCodeBlocks() { }; } -const myTheme = createCssVariablesTheme({ - name: "css-variables", - variablePrefix: "--shiki-", - variableDefaults: {}, - fontStyle: true, -}); - const highlighter = await createHighlighter({ langs: [ "typescript", @@ -42,9 +35,11 @@ const highlighter = await createHighlighter({ "php", "python", ], - themes: [myTheme], + themes: [], }); +highlighter.loadTheme("dark-plus"); + function rehypeShiki() { return async (tree) => { visit(tree, "element", (node) => { @@ -60,7 +55,7 @@ function rehypeShiki() { if (node.properties.language) { const html = highlighter.codeToHtml(textNode.value, { lang: node.properties.language, - theme: myTheme, + theme: "dark-plus", transformers: [ transformerNotationFocus(), transformerNotationHighlight(), From 170c4e2ea26d3426703ac7776feebe2b6687ea03 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 14:18:32 +0200 Subject: [PATCH 028/110] docs: :memo: Begin work on Signatures documentation --- app/signatures/page.mdx | 111 ++++++++++++++++++++++++++++++++++++++ components/Navigation.tsx | 1 + 2 files changed, 112 insertions(+) create mode 100644 app/signatures/page.mdx diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx new file mode 100644 index 0000000..95d1f55 --- /dev/null +++ b/app/signatures/page.mdx @@ -0,0 +1,111 @@ +export const metadata = { + title: 'Signatures', + description: + 'Learn how signatures work, and how to implement them in your Lysand server.', +} + +# Signatures + +Lysand uses cryptographic signatures to ensure the integrity and authenticity of data. Signatures are used to verify that the data has not been tampered with and that it was created by the expected user. {{ className: 'lead' }} + + + This part is very important! If signatures are implemented incorrectly in your server, **you will not be able to federate**. + + Mistakes made in this section can lead to **security vulnerabilities** and **impersonation attacks**. + + +## Signature Definition + +A signature is encoded the same way that Mastodon does it. It consists of a series of HTTP headers in a request. The following headers are used: +- **`Signature`**: The signature itself, encoded in the following format: `keyId="$keyId",algorithm="$algorithm",headers="$headers",signature="$signature"`. + - `keyId`: URI of the user that signed the request. + - `algorithm`: Algorithm used to sign the request. Currently, only `ed25519` is supported. + - `headers`: List of headers that were signed. Should always be `(request-target) host date digest`. + - `signature`: The signature itself, encoded in Base64. + +### Calculating the Signature + +Create a string containing the following (including newlines): +``` +(request-target): $0 $1 +host: $2 +date: $3 +digest: SHA-256=$4 +``` + + + The last line of the string MUST be terminated with a newline character (`\n`). + + +Where: +- `$0` is the HTTP method (e.g. `GET`, `POST`) in lowercase. +- `$1` is the path of the request, in standard URI format (don't forget to URL-encode it). +- `$2` is the hostname of the server (e.g. `example.com`, not `https://example.com` or `example.com/`). +- `$3` is the date and time of the request, formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string. +- `$4` is the SHA-256 hash of the request body, encoded in Base64. + +Sign this string using the user's private key. The resulting signature should be encoded in Base64. + +### Example + +The following example is written in TypeScript using the WebCrypto API. + +`@bob`, from `bob.com`, wants to sign a request to `alice.com`. The request is a `POST` to `/notes`, with the following body: +```json +{ + "content": "Hello, world!" +} +``` + +Bob can be found at `https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511`. His ed25519 private key, encoded in Base64 PKCS8, is `MC4CAQAwBQYDK2VwBCIEILrNXhbWxC/MhKQDsJOAAF1FH/R+Am5G/eZKnqNum5ro`. + +Here's how Bob would sign the request: +```typescript +const content = JSON.stringify({ + content: "Hello, world!", +}); + +const base64PrivateKey = "MC4CAQAwBQYDK2VwBCIEILrNXhbWxC/MhKQDsJOAAF1FH/R+Am5G/eZKnqNum5ro"; +const privateKey = await crypto.subtle.importKey( + "pkcs8", + Buffer.from(base64PrivateKey, "base64"), + "Ed25519", + false, + ["sign"], +); + +const date = new Date().toISOString(); +const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(content) +); + +const stringToSign = + `(request-target): post /notes\n` + + `host: alice.com\n` + + `date: ${date}\n` + + `digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`; + +const signature = await crypto.subtle.sign( + "Ed25519", + privateKey, + new TextEncoder().encode(stringToSign) +); + +const base64Signature = Buffer.from(signature).toString("base64"); +``` + +To send the request, Bob would use the following code: + +```typescript +const headers = new Headers(); + +headers.set("Date", date); +headers.set("Signature", `keyId="https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511",algorithm="ed25519",headers="(request-target) host date digest",signature="${base64Signature}"`); + +const response = await fetch("https://alice.com/notes", { + method: "POST", + headers, + body: content, +}); +``` \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 4a02641..48b2f73 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -249,6 +249,7 @@ export const navigation: NavGroup[] = [ { title: "Introduction", href: "/introduction" }, { title: "SDKs", href: "/sdks" }, { title: "Entities", href: "/entities" }, + { title: "Signatures", href: "/signatures" }, ], }, { From 31fb1b8920311f88aecd2f865899a9d850d5e2c4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 14:53:05 +0200 Subject: [PATCH 029/110] docs: :memo: Finish signing docs --- app/signatures/page.mdx | 50 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx index 95d1f55..13d0e96 100644 --- a/app/signatures/page.mdx +++ b/app/signatures/page.mdx @@ -16,12 +16,19 @@ Lysand uses cryptographic signatures to ensure the integrity and authenticity of ## Signature Definition -A signature is encoded the same way that Mastodon does it. It consists of a series of HTTP headers in a request. The following headers are used: +A signature consists of a series of headers in an HTTP request. The following headers are used: - **`Signature`**: The signature itself, encoded in the following format: `keyId="$keyId",algorithm="$algorithm",headers="$headers",signature="$signature"`. - - `keyId`: URI of the user that signed the request. + - `keyId`: URI of the user that signed the request. Must be the Server Actor's URI if this request is not originated by user action. - `algorithm`: Algorithm used to sign the request. Currently, only `ed25519` is supported. - `headers`: List of headers that were signed. Should always be `(request-target) host date digest`. - `signature`: The signature itself, encoded in Base64. +- **`Date`**: Date and time of the request, formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string. + +Signatures are **required on ALL federation traffic**. If a request does not have a signature, it **MUST** be rejected. Specifically, signatures must be put on: +- **All POST requests**. +- **All responses to GET requests** (for example, when fetching a user's profile). In this case, the HTTP method used in the signature must be `GET`. + +If a signature fails, is missing or is invalid, the server **MUST** return a `401 Unauthorized` HTTP status code. ### Calculating the Signature @@ -61,6 +68,11 @@ Bob can be found at `https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511` Here's how Bob would sign the request: ```typescript +/** + * Using Node.js's Buffer API for brevity + * If using another runtime, you may need to use a different method to convert to/from Base64 + */ + const content = JSON.stringify({ content: "Hello, world!", }); @@ -102,10 +114,44 @@ const headers = new Headers(); headers.set("Date", date); headers.set("Signature", `keyId="https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511",algorithm="ed25519",headers="(request-target) host date digest",signature="${base64Signature}"`); +headers.set("Content-Type", "application/json"); const response = await fetch("https://alice.com/notes", { method: "POST", headers, body: content, }); +``` + +On Alice's side, she would verify the signature using Bob's public key. Here, we assume that Alice has Bob's public key stored in a variable called `publicKey` (during real federation, this would be fetched from Bob's profile). + +```typescript +const method = request.method.toLowerCase(); +const path = new URL(request.url).pathname; +const signature = request.headers.get("Signature"); +const date = new Date(request.headers.get("Date")); + +const [keyId, algorithm, headers, signature] = signature.split(",").map((part) => part.split("=")[1].replace(/"/g, "")); + +const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(await request.text()) +); + +const stringToVerify = + `(request-target): ${method} ${path}\n` + + `host: alice.com\n` + + `date: ${date.toISOString()}\n` + + `digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`; + +const isVerified = await crypto.subtle.verify( + "Ed25519", + publicKey, + Buffer.from(signature, "base64"), + new TextEncoder().encode(stringToVerify) +); + +if (!isVerified) { + return new Response("Signature verification failed", { status: 401 }); +} ``` \ No newline at end of file From e03c8d2a6e5111a38861cb4bdace18346ca5af70 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 15:09:44 +0200 Subject: [PATCH 030/110] docs: :memo: Add section on signature verification --- app/signatures/page.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx index 13d0e96..d3cef01 100644 --- a/app/signatures/page.mdx +++ b/app/signatures/page.mdx @@ -53,6 +53,14 @@ Where: Sign this string using the user's private key. The resulting signature should be encoded in Base64. +### Verifying the Signature + +To verify a signature, the server must: +- Recreate the string as described above. +- Extract the signature provided in the `Signature` header (`$signature` in the above section). +- Decode the signature from Base64. +- Perform a signature verification using the user's public key. + ### Example The following example is written in TypeScript using the WebCrypto API. From e5b2da53b4864da061e6ccc8748612d25c475603 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 15:23:48 +0200 Subject: [PATCH 031/110] feat: :lipstick: Increase text font size --- components/Code.tsx | 2 +- components/MobileNavigation.tsx | 2 +- components/mdx.tsx | 2 +- typography.ts | 33 ++++++++++++++++++--------------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/components/Code.tsx b/components/Code.tsx index 2bfbef0..a84f2c5 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -162,7 +162,7 @@ function CodePanel({
    -
    +                
                         {children}
                     
    diff --git a/components/MobileNavigation.tsx b/components/MobileNavigation.tsx index 73c43f0..5a79cca 100644 --- a/components/MobileNavigation.tsx +++ b/components/MobileNavigation.tsx @@ -130,7 +130,7 @@ function MobileNavigationDialog({ > diff --git a/components/mdx.tsx b/components/mdx.tsx index 6a7cd0b..0ceb89f 100644 --- a/components/mdx.tsx +++ b/components/mdx.tsx @@ -13,7 +13,7 @@ export { CodeGroup, Code as code, Pre as pre } from "./Code"; export function wrapper({ children }: { children: ReactNode }) { return ( -
    +
    {children}
    diff --git a/typography.ts b/typography.ts index 505c2e8..789542d 100644 --- a/typography.ts +++ b/typography.ts @@ -44,13 +44,13 @@ export default function typographyStyles({ theme }: PluginUtils) { // Base color: "var(--tw-prose-body)", - fontSize: theme("fontSize.sm")[0], - lineHeight: theme("lineHeight.7"), + fontSize: theme("fontSize.base")[0], + lineHeight: theme("lineHeight.8"), // Text p: { - marginTop: theme("spacing.6"), - marginBottom: theme("spacing.6"), + marginTop: theme("spacing.8"), + marginBottom: theme("spacing.8"), }, '[class~="lead"]': { fontSize: theme("fontSize.base")[0], @@ -174,22 +174,22 @@ export default function typographyStyles({ theme }: PluginUtils) { h1: { color: "var(--tw-prose-headings)", fontWeight: "700", - fontSize: theme("fontSize.2xl")[0], - ...theme("fontSize.2xl")[1], - marginBottom: theme("spacing.2"), + fontSize: theme("fontSize.4xl")[0], + ...theme("fontSize.4xl")[1], + marginBottom: theme("spacing.6"), }, h2: { color: "var(--tw-prose-headings)", fontWeight: "600", - fontSize: theme("fontSize.lg")[0], - ...theme("fontSize.lg")[1], + fontSize: theme("fontSize.2xl")[0], + ...theme("fontSize.2xl")[1], marginTop: theme("spacing.16"), - marginBottom: theme("spacing.2"), + marginBottom: theme("spacing.4"), }, h3: { color: "var(--tw-prose-headings)", - fontSize: theme("fontSize.base")[0], - ...theme("fontSize.base")[1], + fontSize: theme("fontSize.lg")[0], + ...theme("fontSize.lg")[1], fontWeight: "600", marginTop: theme("spacing.10"), marginBottom: theme("spacing.2"), @@ -206,8 +206,8 @@ export default function typographyStyles({ theme }: PluginUtils) { }, figcaption: { color: "var(--tw-prose-captions)", - fontSize: theme("fontSize.xs")[0], - ...theme("fontSize.xs")[1], + fontSize: theme("fontSize.sm")[0], + ...theme("fontSize.sm")[1], marginTop: theme("spacing.2"), }, @@ -302,7 +302,10 @@ export default function typographyStyles({ theme }: PluginUtils) { paddingLeft: theme("padding[1.5]"), boxShadow: "inset 0 0 0 1px var(--tw-prose-code-ring)", backgroundColor: "var(--tw-prose-code-bg)", - fontSize: theme("fontSize.2xs"), + // If gets too long, split into multiple lines + whiteSpace: "pre-wrap", + wordBreak: "break-all", + fontSize: theme("fontSize.xs"), }, ":is(a, h1, h2, h3, blockquote, thead th) code": { color: "inherit", From d8eb4e96e474b54540f899333ae0c60843a42268 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 25 Jul 2024 15:25:55 +0200 Subject: [PATCH 032/110] fix: :lipstick: Fix code block borders being hidden on mobile --- components/Code.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Code.tsx b/components/Code.tsx index a84f2c5..b9bc831 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -328,7 +328,7 @@ export function CodeGroup({ const hasTabs = Children.count(children) > 1; const containerClassName = - "my-6 overflow-hidden rounded-md bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"; + "my-6 overflow-hidden rounded-md bg-zinc-900 shadow-md dark:ring-1 ring-inset dark:ring-white/10"; const header = ( Date: Thu, 25 Jul 2024 15:40:08 +0200 Subject: [PATCH 033/110] fix: :pencil2: Add missing parentheses --- app/structures/content-format/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/structures/content-format/page.mdx b/app/structures/content-format/page.mdx index b182f3b..c11fad2 100644 --- a/app/structures/content-format/page.mdx +++ b/app/structures/content-format/page.mdx @@ -58,7 +58,7 @@ HTML is the recommended content type for text content, and as such every text co Rich formats include: - `text/html` - `text/markdown` -- `text/x.misskeymarkdown` (Misskey Flavoured Markdown, common on ActivityPub +- `text/x.misskeymarkdown` (Misskey Flavoured Markdown, common on ActivityPub) Clients should display the richest possible format available. If the client does not support the richest format, it should fall back to the next richest format. From d1fd5c585c327ff3479834202e8433ae2899c0e4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 27 Jul 2024 15:37:58 +0200 Subject: [PATCH 034/110] docs: :memo: Add docs about federation --- app/federation/http/page.mdx | 78 ++++++++++++++++++++++++++++++ app/federation/page.mdx | 18 +++++++ app/federation/validation/page.mdx | 31 ++++++++++++ app/introduction/page.mdx | 3 -- app/page.tsx | 2 +- components/ExperimentalWarning.tsx | 4 +- components/Footer.tsx | 14 ++++-- components/Guides.tsx | 53 ++++++++++---------- components/Header.tsx | 5 +- components/MobileNavigation.tsx | 4 ++ components/Navigation.tsx | 10 +++- components/Search.tsx | 4 ++ mdx/rehype.mjs | 1 + typography.ts | 2 +- 14 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 app/federation/http/page.mdx create mode 100644 app/federation/page.mdx create mode 100644 app/federation/validation/page.mdx diff --git a/app/federation/http/page.mdx b/app/federation/http/page.mdx new file mode 100644 index 0000000..355edaf --- /dev/null +++ b/app/federation/http/page.mdx @@ -0,0 +1,78 @@ +export const metadata = { + title: 'HTTP', + description: + 'How Lysand uses the HTTP protocol for all communications between instances.', +} + +# HTTP + +Lysand uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. + +## Communication + +ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/users)'s private key or the [Server Actor](/entities/server-actor)'s private key. + +## Requests + + + + + + Must include `application/json`. + + + Must include `application/json; charset=utf-8`, if the request has a body. + + + Request signature, if the request is signed. + + + Date and time of the request. + + + A string identifying the software making the request. + + + + + ```http {{ 'title': 'Example Request' }} + POST /users/1/inbox HTTP/1.1 + Accept: application/json + Signature: keyId="https://example.com/users/1",algorithm="ed25519",headers="(request-target) host date digest",signature="..." + Date: Thu, 01 Jan 1970 00:00:00 GMT + User-Agent: CoolServer/1.0 (https://coolserver.com) + ``` + + + +## Responses + + + + + + Must include `application/json; charset=utf-8`. + + + Response signature, if the response is signed. + + + Date and time of the response. + + + Must include `no-store` on entities that can be edited directly without a `Patch`, such as `Users`. + + **SHOULD** include a large `max-age` on entities that are not expected to change frequently, such as `Notes`, or CDN resources. + + + + + ```http {{ 'title': 'Example Response' }} + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + Date: Thu, 01 Jan 1970 00:00:00 GMT + Signature: keyId="https://example.com/users/1",algorithm="ed25519",headers="(request-target) host date digest",signature="..." + Cache-Control: no-store + ``` + + \ No newline at end of file diff --git a/app/federation/page.mdx b/app/federation/page.mdx new file mode 100644 index 0000000..e3c6dd7 --- /dev/null +++ b/app/federation/page.mdx @@ -0,0 +1,18 @@ +import { Guides, Guide } from '@/components/Guides'; + +export const metadata = { + title: 'Federation', + description: + 'Description of federation behavior in Lysand.', +} + +# Federation + +Being a federation protocol, Lysand defines a set of rules for exchanging data between instances. This document outlines the behavior of instances in a Lysand federated network. {{ className: 'lead' }} + +Federation is built on the [HyperText Transfer Protocol (HTTP)](https://tools.ietf.org/html/rfc7230) and the [JavaScript Object Notation (JSON)](https://tools.ietf.org/html/rfc7159) data format. Instances communicate with each other by sending and receiving JSON payloads over HTTP. + + + + + \ No newline at end of file diff --git a/app/federation/validation/page.mdx b/app/federation/validation/page.mdx new file mode 100644 index 0000000..813205e --- /dev/null +++ b/app/federation/validation/page.mdx @@ -0,0 +1,31 @@ +export const metadata = { + title: 'Validation', + description: + 'Validation rules for Lysand implementations.', +} + +# Validation + +Implementations **MUST** strictly validate all incoming data to ensure that it is well-formed and adheres to the Lysand Protocol. If a request is invalid, the server **MUST** return a `400 Bad Request` HTTP status code. + + + Remember that while *your* implementation may disallow or restrict some user input, other implementations may not. You **should not** apply those restrictions to data coming from other instances. + + For example, if your implementation disallows using HTML in posts, you should not strip HTML from posts coming from other instances. You *may* choose to display them differently, but you should not modify the data itself. + + +Things that should be validated include, but are not limited to: + +- The presence of **all required fields**. +- The **format** of all fields (integers should not be strings, dates should be in ISO 8601 format, etc.). +- The presence of **all required headers**. +- The presence of a **valid signature**. +- The **length** of all fields (for example, the `username` field on a `User` entity should be at least 1 character long). + - Do not set arbitrary limits on the length of fields that other instances may send you. For example, a `bio` field should not be limited to 160 characters, even if your own implementation has such a limit. + - If you do set limits, they should be reasonable and well-documented. +- The **type**, **precision** and **scale** of all numeric fields. + - For example, a `size` field on a `ContentFormat` structure should be a positive integer, not a negative number or a floating-point number. +- The **validity** of all URLs and URIs (run them through your favorite URL parser). +- The **time** of all dates and times (people should not be born in the future, or in the year 0). + +It is your implementation's duty to reject data from other instances that does not adhere to the strict spec. **This is crucial to ensure the integrity of your instance and the network as a whole**. Allowing data that is technically valid but semantically incorrect can lead to the degradation of the entire Lysand ecosystem. \ No newline at end of file diff --git a/app/introduction/page.mdx b/app/introduction/page.mdx index 3cb16f8..f67406e 100644 --- a/app/introduction/page.mdx +++ b/app/introduction/page.mdx @@ -1,4 +1,3 @@ -import { Guides } from '@/components/Guides' import { Resources } from '@/components/Resources' import { HeroPattern } from '@/components/HeroPattern' @@ -50,6 +49,4 @@ The Lysand Protocol is heavily inspired by the [ActivityPub](https://www.w3.org/ - **Signatures**: Most types of interactions **must** be signed with a private key. Unlike other protocols, signatures are **mandatory**, not optional. - **Developer-Friendliness**: Understanding and implementing your own Lysand server should be easy. Documentation is aimed at developers first. -{/* */} - diff --git a/app/page.tsx b/app/page.tsx index 1e40eca..1aa3510 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -63,7 +63,7 @@ const Page: FC = () => { children: ( <>
    -

    +

    Lysand

    diff --git a/components/ExperimentalWarning.tsx b/components/ExperimentalWarning.tsx index 08bddc2..52d1518 100644 --- a/components/ExperimentalWarning.tsx +++ b/components/ExperimentalWarning.tsx @@ -2,7 +2,7 @@ import type { FC } from "react"; export const ExperimentalWarning: FC = () => ( <> -
    +
    + ); diff --git a/components/Footer.tsx b/components/Footer.tsx index ef22551..1bf9679 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -87,7 +87,12 @@ function SocialLink({ children: ReactNode; }) { return ( - + {children}
    - - Follow us on GitHub + + Find us on GitHub

    diff --git a/components/Guides.tsx b/components/Guides.tsx index c4bb00f..fa91594 100644 --- a/components/Guides.tsx +++ b/components/Guides.tsx @@ -1,41 +1,38 @@ +import type { ReactNode } from "react"; import { Button } from "./Button"; import { Heading } from "./Heading"; -const guides = [ - { - href: "/authentication", - name: "Authentication", - description: "Learn how to authenticate your API requests.", - }, -]; - -export function Guides() { +export function Guides({ children }: { children: ReactNode }) { return (
    Guides
    - {guides.map((guide) => ( -
    -

    - {guide.name} -

    -

    - {guide.description} -

    -

    - -

    -
    - ))} + {children}
    ); } + +export function Guide({ + href, + name, + description, +}: { href: string; name: string; description: string }) { + return ( +
    +

    + {name} +

    +

    + {description} +

    +

    + +

    +
    + ); +} diff --git a/components/Header.tsx b/components/Header.tsx index f12d60c..e1eccd4 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -80,7 +80,10 @@ export const Header = forwardRef, { className?: string }>(
    -
    + +

    Team

    + +
    + + + +
    ), }); diff --git a/components/Team.tsx b/components/Team.tsx new file mode 100644 index 0000000..4e98a41 --- /dev/null +++ b/components/Team.tsx @@ -0,0 +1,56 @@ +import { Icon } from "@iconify-icon/react/dist/iconify.mjs"; +import type { FC } from "react"; + +export const TeamMember: FC<{ + name: string; + bio?: string; + username?: string; + avatarUrl: string; + socials?: { + name: string; + url: string; + icon: string; + }[]; +}> = ({ name, bio, socials, avatarUrl, username }) => { + return ( +
    +
    + +
    +
    + {`${name}'s +
    +
    + +
    +

    {name}

    + @{username} +
    +
    + +
    {bio}
    + +
      + {socials?.map((social) => ( +
    • + + + +
    • + ))} +
    +
    + ); +}; From b4c4254797738dc358cbd5039fd19ee41a130a6d Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 03:41:02 +0200 Subject: [PATCH 064/110] feat: :fire: Remove Patches --- app/entities/patches/page.mdx | 31 ------------------------------- components/Navigation.tsx | 1 - 2 files changed, 32 deletions(-) delete mode 100644 app/entities/patches/page.mdx diff --git a/app/entities/patches/page.mdx b/app/entities/patches/page.mdx deleted file mode 100644 index eb8fce0..0000000 --- a/app/entities/patches/page.mdx +++ /dev/null @@ -1,31 +0,0 @@ -export const metadata = { - title: 'Patches', - description: 'Definition of the Patch entity', -} - -# Patches - -Patches are entities that indicate a change to another entity. They are sent to instances as a kind of "reindex this entity" message. Patches are used to update entities that have already been distributed to other instances. - -## Entity Definition - - - - - - URI of the entity that was patched. Must be an absolute URI on the same instance. - - - - - ```jsonc {{ 'title': 'Example Patch (deletes attachments from the previous page\\'s Note)' }} - { - "id": "be96bb7a-4b8e-45f9-93aa-23633eb3cfdc", - "type": "Patch", // [!code focus] - "uri": "https://social.lysand.org/objects/be96bb7a-4b8e-45f9-93aa-23633eb3cfdc", - "created_at": "2024-06-19T01:07:44.139Z", - "patched": "https://social.lysand.org/notes/9a8928b6-2526-4979-aab1-ef2f88cd5700", // [!code focus] - } - ``` - - \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 77146ed..8f1eebd 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -273,7 +273,6 @@ export const navigation: NavGroup[] = [ links: [ { title: "Users", href: "/entities/users" }, { title: "Notes", href: "/entities/notes" }, - { title: "Patches", href: "/entities/patches" }, ], }, { From 5f614a4dfc46799c250954eb162e37a2880d1aca Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 03:44:17 +0200 Subject: [PATCH 065/110] refactor: :memo: URI spec changes --- app/entities/page.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 9fc9f04..65ac1c1 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -33,6 +33,8 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. URI of the entity. Should be unique and resolve to the entity. Must be an absolute URI. + + **Some entity types may not need a URI. This will be specified in the entity's documentation.** Extensions to the entity. Use this to add custom properties to the entity. From 909b7ec115f41f6589eff8948d334dc030ecaa40 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 03:58:28 +0200 Subject: [PATCH 066/110] feat: :sparkles: Add changelog --- app/changelog/page.mdx | 28 ++++++++++++++++++++++++++++ components/Header.tsx | 2 +- components/Navigation.tsx | 6 +++++- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 app/changelog/page.mdx diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx new file mode 100644 index 0000000..5876d64 --- /dev/null +++ b/app/changelog/page.mdx @@ -0,0 +1,28 @@ +export const metadata = { + title: 'Changelog', + description: + 'Changes since the last version of the Lysand protocol.', +} + +# Changelog + +This page lists changes since Working Draft 03. {{ className: 'lead' }} + +## Since WD 03 + +- Rewrote the signature system from scratch to be simpler and not depend on dates. +- Moved Likes and Dislikes to an extension. +- Renamed fields on several common entities like [Users](/entities/users) and [Notes](/entities/notes). +- Removed the `Patch` entity. + - Useless since edits can just be sent to inboxes directly. +- Allowed `uri`s to not contain the entity's `id`. +- Loosened restrictions on the `id` field in entities. + - Can now be any string, not just a UUID. +- Made `uri` optional in some entities not useful enough to keep track of. +- Added shared inboxes. +- Added `remote` field to [ContentFormat](/structures/content-format). +- Switched to [ThumbHash](https://evanw.github.io/thumbhash/) from [BlurHash](https://blurha.sh/). +- Added optional identification characters to [Custom Emojis](/structures/emoji). +- Added `manually_approves_followers` to [Users](/entities/users). +- Removed `visibility` from [Notes](/entities/notes). +- Made `subject` optional in [Notes](/entities/notes). \ No newline at end of file diff --git a/components/Header.tsx b/components/Header.tsx index 90b334a..455e1e2 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -94,7 +94,7 @@ export const Header = forwardRef, { className?: string }>(
    - +
    diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 8f1eebd..f7b9807 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -298,7 +298,11 @@ export function Navigation(props: ComponentPropsWithoutRef<"nav">) { /> ))}
  • -
  • From 4df2d8194dafafe98f7ea56c7d2631fe08fe4ee3 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 15:51:25 +0200 Subject: [PATCH 067/110] feat: :memo: Document the Follow stack --- app/entities/follow-accepts/page.mdx | 42 ++++++++++++++ app/entities/follow-rejects/page.mdx | 52 ++++++++++++++++++ app/entities/follows/page.mdx | 82 ++++++++++++++++++++++++++++ app/entities/unfollows/page.mdx | 57 +++++++++++++++++++ components/Navigation.tsx | 4 ++ 5 files changed, 237 insertions(+) create mode 100644 app/entities/follow-accepts/page.mdx create mode 100644 app/entities/follow-rejects/page.mdx create mode 100644 app/entities/follows/page.mdx create mode 100644 app/entities/unfollows/page.mdx diff --git a/app/entities/follow-accepts/page.mdx b/app/entities/follow-accepts/page.mdx new file mode 100644 index 0000000..1748d3d --- /dev/null +++ b/app/entities/follow-accepts/page.mdx @@ -0,0 +1,42 @@ +export const metadata = { + title: 'FollowAccept', + description: 'FollowAccept lets users accept follow requests', +} + +# FollowAccept + + + Refer to the [Follow](/entities/follow) entity for information on how follow relationships work. + + +## Entity Definition + + + + + + This entity does not have a URI. + + + URI of the `User` considered the 'followee', i.e. the user who is being followed. + + + URI of the `User` considered the 'follower', i.e. the user who is trying to follow the author. + + + + + + + ```jsonc {{ title: 'Example FollowAccept' }} + { + "type": "FollowAccept", + "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "created_at": "2021-01-01T00:00:00.000Z", + "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" + } + ``` + + + \ No newline at end of file diff --git a/app/entities/follow-rejects/page.mdx b/app/entities/follow-rejects/page.mdx new file mode 100644 index 0000000..bfba74b --- /dev/null +++ b/app/entities/follow-rejects/page.mdx @@ -0,0 +1,52 @@ +export const metadata = { + title: 'FollowReject', + description: 'FollowReject lets users reject follow requests', +} + +# FollowReject + + + Refer to the [Follow](/entities/follow) entity for information on how follow relationships work. + + +## Removing followers + +`FollowReject` can also be used *after* a follow relationship has been established to remove a follower. + +For example, if Bob requests to follow Alice, this entity is used when: +- Alice wants to reject Bob's follow request. + +But it can also be used when Bob is already following Alice, in the case that: +- Alice wants to remove Bob as a follower. + +## Entity Definition + + + + + + This entity does not have a URI. + + + URI of the `User` considered the 'followee', i.e. the user who is being followed. + + + URI of the `User` considered the 'follower', i.e. the user who is trying to follow the author. + + + + + + + ```jsonc {{ title: 'Example FollowReject' }} + { + "type": "FollowReject", + "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "created_at": "2021-01-01T00:00:00.000Z", + "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" + } + ``` + + + \ No newline at end of file diff --git a/app/entities/follows/page.mdx b/app/entities/follows/page.mdx new file mode 100644 index 0000000..655c0cd --- /dev/null +++ b/app/entities/follows/page.mdx @@ -0,0 +1,82 @@ +export const metadata = { + title: 'Follow', + description: 'The Follow entity allows users to subscribe to each other', +} + +# Follow + +Sometimes, [Users](/entities/users) want to subscribe to each other to see each other's content. The `Follow` entity facilitates this, by definining a subscription relationship between two users. {{ className: 'lead' }} + +## Vocabulary + +- **Follower**: The user who is subscribing to another user. If `Joe` is following `Alice`, `Joe` is the follower. +- **Followee**: The user who is being subscribed to. If `Joe` is following `Alice`, `Alice` is the followee. +- **Subscribing**: Identical to **Following**. The act of subscribing to another user's content. + +## Usage + +Consider the following example: + +`Joe`, a user on `social.joe.org`, wants to follow `Alice`, a user on `alice.dev`. + +### Sending a Follow Request + +To establish a follow relationship, `social.joe.org` can do the following: + +1. Create a `Follow` entity with `Joe` as the author and `Alice` as the followee. +2. Send the `Follow` entity to `Alice`'s inbox. +3. Mark the relationship as "processing" in its database until `Alice` accepts the follow request. + +### Accepting the Follow Request + +To accept the follow request, `Alice` can do the following: + +1. Create a [FollowAccept](/entities/follow-accepts) entity with `Alice` as the author and `Joe` as the follower. +2. Send the `FollowAccept` entity to `Joe`'s inbox. + +### Rejecting the Follow Request + +To reject the follow request, `Alice` can do the following: + +1. Create a [FollowReject](/entities/follow-rejects) entity with `Alice` as the author and `Joe` as the follower. +2. Send the `FollowReject` entity to `Joe`'s inbox. + +### Final Steps + +Depending on whether the follow request is accepted or rejected, `social.joe.org` can then update the relationship status accordingly in its database. + +## Behaviour + +Once a follow relationship is established, the **followee**'s instance should send all new notes from the **followee** to the **follower**'s inbox. + +## Entity Definition + + + + + + This entity does not have a URI. + + + URI of the `User` considered the 'follower'. + + + URI of the `User` that is being followed. + + + + + + + ```jsonc {{ title: 'Example Follow' }} + { + "type": "Follow", + "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "created_at": "2021-01-01T00:00:00.000Z", + "followee": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" + } + ``` + + + \ No newline at end of file diff --git a/app/entities/unfollows/page.mdx b/app/entities/unfollows/page.mdx new file mode 100644 index 0000000..23697b0 --- /dev/null +++ b/app/entities/unfollows/page.mdx @@ -0,0 +1,57 @@ +export const metadata = { + title: 'Unfollow', + description: 'The Unfollow entity allows users to unsubscribe from each other', +} + +# Unfollow + +Sometimes, [Users](/entities/users) want to unsubscribe from each other to stop seeing each other's content. The `Unfollow` defines such a change. {{ className: 'lead' }} + + + Refer to the [Follow](/entities/follow) entity for information on how follow relationships work. + + + + This is **not** used to remove a follower from a followee. + + For example, if Bob follows Alice, this entity is used when: + - Bob wants to stop following Alice. + + **NOT** when: + + - Alice wants to remove Bob as a follower. + + For the latter, use [FollowReject](/entities/follow-rejects). + + +## Entity Definition + + + + + + This entity does not have a URI. + + + URI of the `User` considered the 'follower', i.e. the user who is unsubscribing from the followee. + + + URI of the `User` considered the 'followee', i.e. the user who is being unsubscribed from. + + + + + + + ```jsonc {{ title: 'Example Unfollow' }} + { + "type": "Unfollow", + "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "created_at": "2021-01-01T00:00:00.000Z", + "followee": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" + } + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index f7b9807..b2241a7 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -273,6 +273,10 @@ export const navigation: NavGroup[] = [ links: [ { title: "Users", href: "/entities/users" }, { title: "Notes", href: "/entities/notes" }, + { title: "Follow", href: "/entities/follows" }, + { title: "FollowAccept", href: "/entities/follow-accepts" }, + { title: "FollowReject", href: "/entities/follow-rejects" }, + { title: "Unfollow", href: "/entities/unfollows" }, ], }, { From da6d9f87b0e0afaa121daa82023ed521a323c424 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 15:53:25 +0200 Subject: [PATCH 068/110] refactor: :memo: Add new changes to changelog --- app/changelog/page.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 5876d64..ac3d8b2 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -25,4 +25,7 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Added optional identification characters to [Custom Emojis](/structures/emoji). - Added `manually_approves_followers` to [Users](/entities/users). - Removed `visibility` from [Notes](/entities/notes). -- Made `subject` optional in [Notes](/entities/notes). \ No newline at end of file +- Made `subject` optional in [Notes](/entities/notes). +- Clarified the way [Follows](/entities/follows) work. +- Removed the use of `Undo` entities for anything except than deleting entities. +- Added [Unfollow](/entities/unfollows) entity. \ No newline at end of file From 19d8d4cdbc984f6b5bff1029ac86ff4dc12caadd Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 11 Aug 2024 15:54:35 +0200 Subject: [PATCH 069/110] chore: :arrow_up: Upgrade dependencies --- bun.lockb | Bin 193616 -> 193616 bytes package.json | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lockb b/bun.lockb index 7e0e5c8b04cdb522885a20f3e7d30c3be077e3e1..bc8547b139cc844e1d4ef4f12b0573aeafed39a7 100755 GIT binary patch delta 7797 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 IocJi Date: Sun, 11 Aug 2024 16:06:48 +0200 Subject: [PATCH 070/110] feat: :dizzy: Add graph to follows --- app/entities/follows/page.mdx | 12 +++++++++++- public/graphs/how-follows-work.svg | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 public/graphs/how-follows-work.svg diff --git a/app/entities/follows/page.mdx b/app/entities/follows/page.mdx index 655c0cd..f2eff80 100644 --- a/app/entities/follows/page.mdx +++ b/app/entities/follows/page.mdx @@ -79,4 +79,14 @@ Once a follow relationship is established, the **followee**'s instance should se ``` - \ No newline at end of file + + +## Graph + +

    + View the full-size graph +

    + +
    + How Follows Work +
    diff --git a/public/graphs/how-follows-work.svg b/public/graphs/how-follows-work.svg new file mode 100644 index 0000000..0443a8e --- /dev/null +++ b/public/graphs/how-follows-work.svg @@ -0,0 +1,13 @@ + + + + + + + + BobAliceFollowFollowerFolloweeStatusFollowAcceptFollowerFolloweeStatusBobAliceAcceptedAliceBobFollowRejectFollowerFolloweeStatusAliceBobFollowRejectAliceBobUnfollowBobAliceBobAlicePendingAliceBobAcceptedAliceBobAcceptedAliceBobAcceptedFollowerFolloweeStatusAliceBobAcceptedFollowerFolloweeStatusAliceBobAccepted \ No newline at end of file From 4b773e114aee4b4999f4db645b146fea23fa9920 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 13 Aug 2024 15:59:25 +0200 Subject: [PATCH 071/110] feat: :memo: Document Delete entity --- app/changelog/page.mdx | 1 + app/entities/deletes/page.mdx | 48 ++++++++++++++++++++++++++++++ app/entities/page.mdx | 5 ++-- app/extensions/likes/page.mdx | 2 +- app/extensions/websockets/page.mdx | 2 +- components/Navigation.tsx | 5 ++-- 6 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 app/entities/deletes/page.mdx diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index ac3d8b2..d61857c 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -28,4 +28,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Made `subject` optional in [Notes](/entities/notes). - Clarified the way [Follows](/entities/follows) work. - Removed the use of `Undo` entities for anything except than deleting entities. +- Renamed `Undo` to [Delete](/entities/deletes). - Added [Unfollow](/entities/unfollows) entity. \ No newline at end of file diff --git a/app/entities/deletes/page.mdx b/app/entities/deletes/page.mdx new file mode 100644 index 0000000..a40dc5f --- /dev/null +++ b/app/entities/deletes/page.mdx @@ -0,0 +1,48 @@ +export const metadata = { + title: 'Delete', + description: 'Deletes are used to remove entities from the system', +} + +# Delete + +Signals the deletion of an entity. {{ className: 'lead' }} + +## Authorization + +Implementations **must** ensure that the author of the `Delete` entity has the authorization to delete the target entity. + +Having the authorization is defined as: +- The author is the creator of the target entity (including [delegation](/delegation)). +- The author is the server actor. + +## Entity Definition + + + + + + This entity does not have a URI. + + + URI of the `User` who is deleting the entity. + + + URI of the entity being deleted. + + + + + + + ```jsonc {{ title: 'Example Delete' }} + { + "type": "Delete", + "id": "9b3212b8-529c-435a-8798-09ebbc17ca74", + "created_at": "2021-01-01T00:00:00.000Z", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "deleted": "https://example.com/notes/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" + } + ``` + + + \ No newline at end of file diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 65ac1c1..4a1f4b0 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -48,11 +48,10 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. ```jsonc {{ 'title': 'Example Entity' }} { "id": "9a8928b6-2526-4979-aab1-ef2f88cd5700", - "type": "Undo", + "type": "Delete", "created_at": "2022-01-01T12:00:00Z", - "uri": "https://bongo.social/objects/9a8928b6-2526-4979-aab1-ef2f88cd5700", "author": "https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e", - "object": "https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b", + "deleted": "https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b", } ``` diff --git a/app/extensions/likes/page.mdx b/app/extensions/likes/page.mdx index 1245894..277dc0e 100644 --- a/app/extensions/likes/page.mdx +++ b/app/extensions/likes/page.mdx @@ -88,7 +88,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis ## Undoing Likes and Dislikes -To undo a like or dislike, an [Undo](/entities/undo) entity should be used. The `object` property of the Undo entity should link to the Like or Dislike entity to be removed. +To undo a like or dislike, a [Delete](/entities/deletes) entity should be used. The `deleted` property of the Delete entity should link to the Like or Dislike entity to be removed. ## User Collections diff --git a/app/extensions/websockets/page.mdx b/app/extensions/websockets/page.mdx index 618fc28..0897207 100644 --- a/app/extensions/websockets/page.mdx +++ b/app/extensions/websockets/page.mdx @@ -45,7 +45,7 @@ Messages sent over the WebSocket connection are JSON objects. "signature": "post /users/1/inbox a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "nonce": "a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341", "signed_by": "https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e", - "entity": "{\"id\":\"9a8928b6-2526-4979-aab1-ef2f88cd5700\",\"type\":\"Undo\",\"created_at\":\"2022-01-01T12:00:00Z\",\"uri\":\"https://bongo.social/objects/9a8928b6-2526-4979-aab1-ef2f88cd5700\",\"author\":\"https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e\",\"object\":\"https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b\"}" + "entity": "{\"id\":\"9a8928b6-2526-4979-aab1-ef2f88cd5700\",\"type\":\"Delete\",\"created_at\":\"2022-01-01T12:00:00Z\",\"author\":\"https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e\",\"deleted\":\"https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b\"}" } ``` diff --git a/components/Navigation.tsx b/components/Navigation.tsx index b2241a7..4bd2713 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -271,12 +271,13 @@ export const navigation: NavGroup[] = [ { title: "Entities", links: [ - { title: "Users", href: "/entities/users" }, - { title: "Notes", href: "/entities/notes" }, + { title: "Delete", href: "/entities/deletes" }, { title: "Follow", href: "/entities/follows" }, { title: "FollowAccept", href: "/entities/follow-accepts" }, { title: "FollowReject", href: "/entities/follow-rejects" }, + { title: "Notes", href: "/entities/notes" }, { title: "Unfollow", href: "/entities/unfollows" }, + { title: "Users", href: "/entities/users" }, ], }, { From ff57ee4ffd0fb3ba5129e88776650f09799c1fde Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 13 Aug 2024 16:10:18 +0200 Subject: [PATCH 072/110] feat: :sparkles: Add openGraph and keywords to page metadata --- app/layout.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index a29bffb..abf8d95 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,7 @@ import type { Section } from "../components/SectionProvider"; import { Providers } from "./providers"; import "@/styles/tailwind.css"; +import logo from "@/images/branding/logo.webp"; import type { ReactNode } from "react"; export const metadata: Metadata = { @@ -13,6 +14,18 @@ export const metadata: Metadata = { template: "%s • Lysand API Reference", default: "Lysand API Reference", }, + keywords: ["federation", "api", "reference", "documentation", "lysand"], + metadataBase: new URL("https://dev.lysand.org"), + openGraph: { + type: "article", + images: { + url: logo.src, + alt: "Lysand logo", + height: logo.height, + width: logo.width, + type: "image/webp", + }, + }, }; export default async function RootLayout({ From a106e7acefcf9fdbeff9572b9f6824c08d9263e2 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 13 Aug 2024 16:29:47 +0200 Subject: [PATCH 073/110] refactor: :truck: Make paths under /entities use the singular form --- app/changelog/page.mdx | 14 +++++++------- app/entities/{deletes => delete}/page.mdx | 0 .../{follow-accepts => follow-accept}/page.mdx | 0 .../{follow-rejects => follow-reject}/page.mdx | 0 app/entities/{follows => follow}/page.mdx | 6 +++--- app/entities/{notes => note}/page.mdx | 4 ++-- app/entities/page.mdx | 2 +- app/entities/{unfollows => unfollow}/page.mdx | 4 ++-- app/entities/{users => user}/page.mdx | 8 ++++---- app/extensions/likes/page.mdx | 8 ++++---- app/federation/http/page.mdx | 2 +- app/introduction/page.mdx | 2 +- components/Navigation.tsx | 14 +++++++------- 13 files changed, 32 insertions(+), 32 deletions(-) rename app/entities/{deletes => delete}/page.mdx (100%) rename app/entities/{follow-accepts => follow-accept}/page.mdx (100%) rename app/entities/{follow-rejects => follow-reject}/page.mdx (100%) rename app/entities/{follows => follow}/page.mdx (86%) rename app/entities/{notes => note}/page.mdx (94%) rename app/entities/{unfollows => unfollow}/page.mdx (87%) rename app/entities/{users => user}/page.mdx (94%) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index d61857c..d27c535 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -12,7 +12,7 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Rewrote the signature system from scratch to be simpler and not depend on dates. - Moved Likes and Dislikes to an extension. -- Renamed fields on several common entities like [Users](/entities/users) and [Notes](/entities/notes). +- Renamed fields on several common entities like [Users](/entities/user) and [Notes](/entities/note). - Removed the `Patch` entity. - Useless since edits can just be sent to inboxes directly. - Allowed `uri`s to not contain the entity's `id`. @@ -23,10 +23,10 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Added `remote` field to [ContentFormat](/structures/content-format). - Switched to [ThumbHash](https://evanw.github.io/thumbhash/) from [BlurHash](https://blurha.sh/). - Added optional identification characters to [Custom Emojis](/structures/emoji). -- Added `manually_approves_followers` to [Users](/entities/users). -- Removed `visibility` from [Notes](/entities/notes). -- Made `subject` optional in [Notes](/entities/notes). -- Clarified the way [Follows](/entities/follows) work. +- Added `manually_approves_followers` to [Users](/entities/user). +- Removed `visibility` from [Notes](/entities/note). +- Made `subject` optional in [Notes](/entities/note). +- Clarified the way [Follows](/entities/follow) work. - Removed the use of `Undo` entities for anything except than deleting entities. -- Renamed `Undo` to [Delete](/entities/deletes). -- Added [Unfollow](/entities/unfollows) entity. \ No newline at end of file +- Renamed `Undo` to [Delete](/entities/delete). +- Added [Unfollow](/entities/unfollow) entity. \ No newline at end of file diff --git a/app/entities/deletes/page.mdx b/app/entities/delete/page.mdx similarity index 100% rename from app/entities/deletes/page.mdx rename to app/entities/delete/page.mdx diff --git a/app/entities/follow-accepts/page.mdx b/app/entities/follow-accept/page.mdx similarity index 100% rename from app/entities/follow-accepts/page.mdx rename to app/entities/follow-accept/page.mdx diff --git a/app/entities/follow-rejects/page.mdx b/app/entities/follow-reject/page.mdx similarity index 100% rename from app/entities/follow-rejects/page.mdx rename to app/entities/follow-reject/page.mdx diff --git a/app/entities/follows/page.mdx b/app/entities/follow/page.mdx similarity index 86% rename from app/entities/follows/page.mdx rename to app/entities/follow/page.mdx index f2eff80..d5aae8a 100644 --- a/app/entities/follows/page.mdx +++ b/app/entities/follow/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Follow -Sometimes, [Users](/entities/users) want to subscribe to each other to see each other's content. The `Follow` entity facilitates this, by definining a subscription relationship between two users. {{ className: 'lead' }} +Sometimes, [Users](/entities/user) want to subscribe to each other to see each other's content. The `Follow` entity facilitates this, by definining a subscription relationship between two users. {{ className: 'lead' }} ## Vocabulary @@ -31,14 +31,14 @@ To establish a follow relationship, `social.joe.org` can do the following: To accept the follow request, `Alice` can do the following: -1. Create a [FollowAccept](/entities/follow-accepts) entity with `Alice` as the author and `Joe` as the follower. +1. Create a [FollowAccept](/entities/follow-accept) entity with `Alice` as the author and `Joe` as the follower. 2. Send the `FollowAccept` entity to `Joe`'s inbox. ### Rejecting the Follow Request To reject the follow request, `Alice` can do the following: -1. Create a [FollowReject](/entities/follow-rejects) entity with `Alice` as the author and `Joe` as the follower. +1. Create a [FollowReject](/entities/follow-reject) entity with `Alice` as the author and `Joe` as the follower. 2. Send the `FollowReject` entity to `Joe`'s inbox. ### Final Steps diff --git a/app/entities/notes/page.mdx b/app/entities/note/page.mdx similarity index 94% rename from app/entities/notes/page.mdx rename to app/entities/note/page.mdx index 5010018..68d8aa8 100644 --- a/app/entities/notes/page.mdx +++ b/app/entities/note/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Notes -Notes represent a piece of content on a Lysand instance. They can be posted by [Users](/entities/users) and are displayed in a user's feed. Notes can contain text, images, and other media. {{ className: 'lead' }} +Notes represent a piece of content on a Lysand instance. They can be posted by [Users](/entities/user) and are displayed in a user's feed. Notes can contain text, images, and other media. {{ className: 'lead' }} Notes are not just limited to microblogging. They can be used for any kind of content, such as forum posts, blog posts, image posts, video posts, audio posts, and even messaging. @@ -61,7 +61,7 @@ Notes represent a piece of content on a Lysand instance. They can be posted by [ Whether the note contains "sensitive content". This can be used with `subject` as a "content warning" feature. - URIs of [Users](/entities/users) that should be notified of the note. Similar to Twitter's `@` mentions. The note may also contain mentions in the content, however only the mentions in this field should trigger notifications. + URIs of [Users](/entities/user) that should be notified of the note. Similar to Twitter's `@` mentions. The note may also contain mentions in the content, however only the mentions in this field should trigger notifications. Previews for any links in the publication. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every server that sees the publication, creating an accidental DDOS attack. diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 4a1f4b0..de111a6 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -82,4 +82,4 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. When serialized to a string, the JSON representation of an entity should follow the following rules: - Keys must be sorted lexicographically. - Should use UTF-8 encoding. -- Must be **signed** using the relevant [User](/entities/users)'s private key, or the [Server Actor](/entities/server-actor)'s private key if the entity is not associated with a particular user. +- Must be **signed** using the relevant [User](/entities/user)'s private key, or the [Server Actor](/entities/server-actor)'s private key if the entity is not associated with a particular user. diff --git a/app/entities/unfollows/page.mdx b/app/entities/unfollow/page.mdx similarity index 87% rename from app/entities/unfollows/page.mdx rename to app/entities/unfollow/page.mdx index 23697b0..fe51cff 100644 --- a/app/entities/unfollows/page.mdx +++ b/app/entities/unfollow/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Unfollow -Sometimes, [Users](/entities/users) want to unsubscribe from each other to stop seeing each other's content. The `Unfollow` defines such a change. {{ className: 'lead' }} +Sometimes, [Users](/entities/user) want to unsubscribe from each other to stop seeing each other's content. The `Unfollow` defines such a change. {{ className: 'lead' }} Refer to the [Follow](/entities/follow) entity for information on how follow relationships work. @@ -21,7 +21,7 @@ Sometimes, [Users](/entities/users) want to unsubscribe from each other to stop - Alice wants to remove Bob as a follower. - For the latter, use [FollowReject](/entities/follow-rejects). + For the latter, use [FollowReject](/entities/follow-reject). ## Entity Definition diff --git a/app/entities/users/page.mdx b/app/entities/user/page.mdx similarity index 94% rename from app/entities/users/page.mdx rename to app/entities/user/page.mdx index 313ee50..c794892 100644 --- a/app/entities/users/page.mdx +++ b/app/entities/user/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Users -The `User` entity represents an account on a Lysand instance. Users can post [Notes](/entities/notes), follow other users, and interact with content. Users are identified by their `id` property, which is unique within the instance. {{ className: 'lead' }} +The `User` entity represents an account on a Lysand instance. Users can post [Notes](/entities/note), follow other users, and interact with content. Users are identified by their `id` property, which is unique within the instance. {{ className: 'lead' }} ## Addresses @@ -115,13 +115,13 @@ Instance **must** be the host of the instance the user is on (hostname with opti The user's federation outbox. Refer to the [federation documentation](/federation). ### Followers - User's followers. [Collection](/structures/collection) of [User](/entities/users) entities. + User's followers. [Collection](/structures/collection) of [User](/entities/user) entities. ### Following - Users that the user follows. [Collection](/structures/collection) of [User](/entities/users) entities. + Users that the user follows. [Collection](/structures/collection) of [User](/entities/user) entities. ### Featured - [Notes](/entities/notes) that the user wants to feature (also known as "pin") on their profile. [Collection](/structures/collection) of [Note](/entities/notes) entities. + [Notes](/entities/note) that the user wants to feature (also known as "pin") on their profile. [Collection](/structures/collection) of [Note](/entities/note) entities. diff --git a/app/extensions/likes/page.mdx b/app/extensions/likes/page.mdx index 277dc0e..c5c61ab 100644 --- a/app/extensions/likes/page.mdx +++ b/app/extensions/likes/page.mdx @@ -26,7 +26,7 @@ Likes are a way for users to show appreciation for a note, like Twitter's "heart Creator of the Like. - URI of the note being liked. Must link to a [Note](/entities/notes). + URI of the note being liked. Must link to a [Note](/entities/note). @@ -64,7 +64,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis Creator of the Dislike. - URI of the note being disliked. Must link to a [Note](/entities/notes). + URI of the note being disliked. Must link to a [Note](/entities/note). @@ -88,11 +88,11 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis ## Undoing Likes and Dislikes -To undo a like or dislike, a [Delete](/entities/deletes) entity should be used. The `deleted` property of the Delete entity should link to the Like or Dislike entity to be removed. +To undo a like or dislike, a [Delete](/entities/delete) entity should be used. The `deleted` property of the Delete entity should link to the Like or Dislike entity to be removed. ## User Collections -The Likes extension adds the following collections to the [User](/entities/users) entity: +The Likes extension adds the following collections to the [User](/entities/user) entity: - `likes`: A [Collection](/structures/collection) of all the notes the user has liked. - `dislikes`: A [Collection](/structures/collection) of all the notes the user has disliked. diff --git a/app/federation/http/page.mdx b/app/federation/http/page.mdx index ac862f5..056bad0 100644 --- a/app/federation/http/page.mdx +++ b/app/federation/http/page.mdx @@ -8,7 +8,7 @@ export const metadata = { Lysand uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. -ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/users)'s private key or the [Server Actor](/entities/server-actor)'s private key. +ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [Server Actor](/entities/server-actor)'s private key. ## Requests diff --git a/app/introduction/page.mdx b/app/introduction/page.mdx index f67406e..251cc4f 100644 --- a/app/introduction/page.mdx +++ b/app/introduction/page.mdx @@ -34,7 +34,7 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are us The Lysand Protocol uses the following terms: -- **Entity**: A generic term for any JSON object in the protocol, such as an [Actor](./entities/actors), a [Note](./entities/notes), or a [Like](./entities/likes). Entities are uniquely identified by their `id` property. +- **Entity**: A generic term for any JSON object in the protocol, such as an [Actor](./entities/actors), a [Note](./entities/note), or a [Like](./entities/likes). Entities are uniquely identified by their `id` property. - **Implementation**: A software application that implements the Lysand Protocol. - **Instance**: An application deploying an **Implementation**. - Using the same nomenclature, an ActivityPub Implementation would be `Mastodon`, and an Instance would be `mastodon.social`. diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 4bd2713..046629b 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -271,13 +271,13 @@ export const navigation: NavGroup[] = [ { title: "Entities", links: [ - { title: "Delete", href: "/entities/deletes" }, - { title: "Follow", href: "/entities/follows" }, - { title: "FollowAccept", href: "/entities/follow-accepts" }, - { title: "FollowReject", href: "/entities/follow-rejects" }, - { title: "Notes", href: "/entities/notes" }, - { title: "Unfollow", href: "/entities/unfollows" }, - { title: "Users", href: "/entities/users" }, + { title: "Delete", href: "/entities/delete" }, + { title: "Follow", href: "/entities/follow" }, + { title: "FollowAccept", href: "/entities/follow-accept" }, + { title: "FollowReject", href: "/entities/follow-reject" }, + { title: "Notes", href: "/entities/note" }, + { title: "Unfollow", href: "/entities/unfollow" }, + { title: "Users", href: "/entities/user" }, ], }, { From 6414337a302e9b149a50f63f7ac17afddab5e0be Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 13 Aug 2024 16:47:37 +0200 Subject: [PATCH 074/110] refactor: :truck: Do full rename of Lysand to Versia --- README.md | 6 +++--- app/changelog/page.mdx | 2 +- app/entities/note/page.mdx | 18 +++++++++--------- app/entities/page.mdx | 10 +++++----- app/entities/user/page.mdx | 28 ++++++++++++++-------------- app/extensions/likes/page.mdx | 12 ++++++------ app/extensions/vanity/page.mdx | 6 +++--- app/extensions/websockets/page.mdx | 2 +- app/federation/http/page.mdx | 4 ++-- app/federation/page.mdx | 8 ++++---- app/federation/validation/page.mdx | 6 +++--- app/introduction/page.mdx | 16 ++++++++-------- app/layout.tsx | 10 +++++----- app/page.tsx | 18 +++++++++--------- app/sdks/page.mdx | 8 ++++---- app/signatures/page.mdx | 4 ++-- app/structures/collection/page.mdx | 10 +++++----- app/structures/emoji/page.mdx | 2 +- components/Logo.tsx | 2 +- components/Resources.tsx | 2 +- package.json | 2 +- 21 files changed, 88 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 61ac0d4..9efaf86 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

    - Lysand Logo + Versia Logo

    - Lysand Documentation + Versia Documentation

    @@ -24,7 +24,7 @@

    - Lysand on an iPad + Versia on an iPad

    ## Technologies diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index d27c535..c5e0ffd 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -1,7 +1,7 @@ export const metadata = { title: 'Changelog', description: - 'Changes since the last version of the Lysand protocol.', + 'Changes since the last version of the Versia protocol.', } # Changelog diff --git a/app/entities/note/page.mdx b/app/entities/note/page.mdx index 68d8aa8..c35a6a5 100644 --- a/app/entities/note/page.mdx +++ b/app/entities/note/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Notes -Notes represent a piece of content on a Lysand instance. They can be posted by [Users](/entities/user) and are displayed in a user's feed. Notes can contain text, images, and other media. {{ className: 'lead' }} +Notes represent a piece of content on a Versia instance. They can be posted by [Users](/entities/user) and are displayed in a user's feed. Notes can contain text, images, and other media. {{ className: 'lead' }} Notes are not just limited to microblogging. They can be used for any kind of content, such as forum posts, blog posts, image posts, video posts, audio posts, and even messaging. @@ -97,12 +97,12 @@ Notes represent a piece of content on a Lysand instance. They can be posted by [ { "id": "01902e09-0f8b-72de-8ee3-9afc0cf5eae1", "type": "Note", - "uri": "https://social.lysand.org/objects/01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "uri": "https://versia.social/objects/01902e09-0f8b-72de-8ee3-9afc0cf5eae1", "created_at": "2024-06-19T01:07:44.139Z", "attachments": [ // [!code focus:100] { "image/png": { - "content": "https://cdn.lysand.org/29e810bf4707fef373d886af322089d5db300fce66e4e073efc26827f10825f6/image.webp", + "content": "https://cdn.versia.social/29e810bf4707fef373d886af322089d5db300fce66e4e073efc26827f10825f6/image.webp", "remote": true, "thumbhash": "1QcSHQRnh493V4dIh4eXh1h4kJUI", "description": "", @@ -116,7 +116,7 @@ Notes represent a piece of content on a Lysand instance. They can be posted by [ }, { "image/png": { - "content": "https://cdn.lysand.org/4f87598d377441e78f3c8cfa7bd7d19d61a7470bfe0abcbee6eb1de87279fb3b/image.webp", + "content": "https://cdn.versia.social/4f87598d377441e78f3c8cfa7bd7d19d61a7470bfe0abcbee6eb1de87279fb3b/image.webp", "remote": true, "thumbhash": "3PcNNYSFeXh/d3eld0iHZoZgVwh2", "description": "", @@ -129,14 +129,14 @@ Notes represent a piece of content on a Lysand instance. They can be posted by [ } } ], - "author": "https://social.lysand.org/users/018eb863-753f-76ff-83d6-fd590de7740a", + "author": "https://versia.social/users/018eb863-753f-76ff-83d6-fd590de7740a", "category": "microblog", "content": { "text/html": { - "content": "

    In the next lysand-fe update: account settings, finally!

    " + "content": "

    In the next versia-fe update: account settings, finally!

    " }, "text/plain": { - "content": "In the next lysand-fe update: account settings, finally!" + "content": "In the next versia-fe update: account settings, finally!" } }, "device": { @@ -145,14 +145,14 @@ Notes represent a piece of content on a Lysand instance. They can be posted by [ "url": "https://sk22.github.io/megalodon" }, "extensions": { - "org.lysand:custom_emojis": { + "pub.versia:custom_emojis": { "emojis": [] } }, "group": "public", "is_sensitive": false, "mentions": [], - "subject": "Lysand development" + "subject": "Versia development" } ``` diff --git a/app/entities/page.mdx b/app/entities/page.mdx index de111a6..842b7d4 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -1,16 +1,16 @@ export const metadata = { title: 'Entities', description: - 'Entities are simple JSON objects that represent the core data structures in Lysand.', + 'Entities are simple JSON objects that represent the core data structures in Versia.', } # Entities -Entities are the foundation of the Lysand protocol. A similar concept to entities are the [ActivityStreams](https://www.w3.org/TR/activitystreams-core/) objects, which are used to represent activities in the [ActivityPub](https://www.w3.org/TR/activitypub/) protocol. {{ className: 'lead' }} +Entities are the foundation of the Versia protocol. A similar concept to entities are the [ActivityStreams](https://www.w3.org/TR/activitystreams-core/) objects, which are used to represent activities in the [ActivityPub](https://www.w3.org/TR/activitypub/) protocol. {{ className: 'lead' }} ## Entity Definition -An entity is a simple JSON object that represents a core data structure in Lysand. Entities are used to represent various types of data, such as users, notes, and more. Each entity has a unique `id` property that is used to identify it within the instance. +An entity is a simple JSON object that represents a core data structure in Versia. Entities are used to represent various types of data, such as users, notes, and more. Each entity has a unique `id` property that is used to identify it within the instance. Any field in an entity not marked as `required` may be omitted or set to `null`. @@ -22,7 +22,7 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. Unique identifier for the entity. Must be unique within the instance. Can be any string. Max of 512 UTF-8 characters. - Type of the entity. Only types defined in the Lysand protocol are allowed. Use an [Extension](/extensions) if you want to define custom types. + Type of the entity. Only types defined in the Versia protocol are allowed. Use an [Extension](/extensions) if you want to define custom types. Date and time when the entity was created. Must be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted string. @@ -67,7 +67,7 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. "zlorb_type": "giant", "zlorb_size": "huge" }, - "org.lysand:location": { + "pub.versia:location": { "latitude": 37.7749, "longitude": -122.4194 } diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index c794892..ca7c319 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Users -The `User` entity represents an account on a Lysand instance. Users can post [Notes](/entities/note), follow other users, and interact with content. Users are identified by their `id` property, which is unique within the instance. {{ className: 'lead' }} +The `User` entity represents an account on a Versia instance. Users can post [Notes](/entities/note), follow other users, and interact with content. Users are identified by their `id` property, which is unique within the instance. {{ className: 'lead' }} ## Addresses @@ -19,8 +19,8 @@ Users may be represented by a shorthand address, in the following formats: For example: ``` -@jessew@social.lysand.org -@018ec082-0ae1-761c-b2c5-22275a611771@social.lysand.org +@jessew@versia.social +@018ec082-0ae1-761c-b2c5-22275a611771@versia.social ``` This is similar to an email address or an ActivityPub address. @@ -71,7 +71,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti A header image for the user's profile. Also known as a cover photo or a banner. Must be an image format (`image/*`). - The user's public key. Must follow the [Lysand Public Key](/signatures) format. `actor` may be a URI to another user's profile, in which case this key may allow the user to act on behalf of the other user (see [delegation](/delegation)). + The user's public key. Must follow the [Versia Public Key](/signatures) format. `actor` may be a URI to another user's profile, in which case this key may allow the user to act on behalf of the other user (see [delegation](/delegation)). ```typescript type URI = string; @@ -132,7 +132,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti { "id": "018ec082-0ae1-761c-b2c5-22275a611771", "type": "User", - "uri": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771", + "uri": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771", "created_at": "2024-04-09T01:38:51.743Z", "avatar": { // [!code focus:100] "image/png": { @@ -148,16 +148,16 @@ Instance **must** be the host of the instance the user is on (hostname with opti } }, "collections": { - "featured": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/featured", - "followers": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/followers", - "following": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/following", - "org.lysand:likes/Dislikes": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/dislikes", - "org.lysand:likes/Likes": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/likes", - "outbox": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/outbox", + "featured": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/featured", + "followers": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/followers", + "following": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/following", + "pub.versia:likes/Dislikes": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/dislikes", + "pub.versia:likes/Likes": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/likes", + "outbox": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/outbox", }, "display_name": "April The Pink (limited Sand Edition)", "extensions": { - "org.lysand:custom_emojis": { + "pub.versia:custom_emojis": { "emojis": [] } }, @@ -176,11 +176,11 @@ Instance **must** be the host of the instance the user is on (hostname with opti } ], "header": null, - "inbox": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771/inbox", + "inbox": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/inbox", "indexable": false, "manually_approves_followers": false, "public_key": { - "actor": "https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771", + "actor": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771", "public_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }, "username": "aprl" diff --git a/app/extensions/likes/page.mdx b/app/extensions/likes/page.mdx index c5c61ab..f48f97b 100644 --- a/app/extensions/likes/page.mdx +++ b/app/extensions/likes/page.mdx @@ -20,7 +20,7 @@ Likes are a way for users to show appreciation for a note, like Twitter's "heart - Must be `org.lysand:likes/Like`. + Must be `pub.versia:likes/Like`. Creator of the Like. @@ -37,7 +37,7 @@ Likes are a way for users to show appreciation for a note, like Twitter's "heart { "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", "type": "Extension", - "extension_type": "org.lysand:likes/Like", + "extension_type": "pub.versia:likes/Like", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", "uri": "https://example.com/likes/3e7e4750-afd4-4d99-a256-02f0710a0520", @@ -58,7 +58,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis - Must be `org.lysand:likes/Dislike`. + Must be `pub.versia:likes/Dislike`. Creator of the Dislike. @@ -75,7 +75,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis { "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", "type": "Extension", - "extension_type": "org.lysand:likes/Dislike", + "extension_type": "pub.versia:likes/Dislike", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", "uri": "https://example.com/dislikes/3e7e4750-afd4-4d99-a256-02f0710a0520", @@ -103,7 +103,7 @@ The Likes extension adds the following collections to the [User](/entities/user) ... "collections": { ... - "org.lysand:likes/Likes": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe/likes", - "org.lysand:likes/Dislikes": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe/dislikes" + "pub.versia:likes/Likes": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe/likes", + "pub.versia:likes/Dislikes": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe/dislikes" } } \ No newline at end of file diff --git a/app/extensions/vanity/page.mdx b/app/extensions/vanity/page.mdx index 49f2b6e..437b819 100644 --- a/app/extensions/vanity/page.mdx +++ b/app/extensions/vanity/page.mdx @@ -68,7 +68,7 @@ All properties are optional. Location does not need to be precise, and can be as simple as `+46+002/` (France) or `+48.52+002.20/` (Paris, France). - Lysand profiles that should be considered aliases of this profile. + Versia profiles that should be considered aliases of this profile. @@ -81,7 +81,7 @@ All properties are optional. "type": "User", // ... "extensions": { // [!code focus:100] - "org.lysand:vanity": { + "pub.versia:vanity": { "avatar_overlays": [ { "image/png": { @@ -124,7 +124,7 @@ All properties are optional. "location": "+40.6894-074.0447/", "aliases": [ "https://burger.social/accounts/349ee237-c672-41c1-aadc-677e185f795a", - "https://social.lysand.org/users/f565ef02-035d-4974-ba5e-f62a8558331d" + "https://versia.social/users/f565ef02-035d-4974-ba5e-f62a8558331d" ] } } diff --git a/app/extensions/websockets/page.mdx b/app/extensions/websockets/page.mdx index 0897207..59b716d 100644 --- a/app/extensions/websockets/page.mdx +++ b/app/extensions/websockets/page.mdx @@ -12,7 +12,7 @@ export const metadata = { If testing proves unsuccessful, this draft may be abandoned.
    -Typically, communication between Lysand instances is done via HTTP. However, HTTP suffers from some limitations, such as high latency and heavy overhead for small messages, making it less suitable for exchanging large amounts of entities at acceptable speeds. {{ className: 'lead' }} +Typically, communication between Versia instances is done via HTTP. However, HTTP suffers from some limitations, such as high latency and heavy overhead for small messages, making it less suitable for exchanging large amounts of entities at acceptable speeds. {{ className: 'lead' }} This extension aims to address these limitations by adding support for the exchange of entities using WebSockets. diff --git a/app/federation/http/page.mdx b/app/federation/http/page.mdx index 056bad0..54f6108 100644 --- a/app/federation/http/page.mdx +++ b/app/federation/http/page.mdx @@ -1,12 +1,12 @@ export const metadata = { title: 'HTTP', description: - 'How Lysand uses the HTTP protocol for all communications between instances.', + 'How Versia uses the HTTP protocol for all communications between instances.', } # HTTP -Lysand uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. +Versia uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [Server Actor](/entities/server-actor)'s private key. diff --git a/app/federation/page.mdx b/app/federation/page.mdx index e3c6dd7..c770bd0 100644 --- a/app/federation/page.mdx +++ b/app/federation/page.mdx @@ -3,16 +3,16 @@ import { Guides, Guide } from '@/components/Guides'; export const metadata = { title: 'Federation', description: - 'Description of federation behavior in Lysand.', + 'Description of federation behavior in Versia.', } # Federation -Being a federation protocol, Lysand defines a set of rules for exchanging data between instances. This document outlines the behavior of instances in a Lysand federated network. {{ className: 'lead' }} +Being a federation protocol, Versia defines a set of rules for exchanging data between instances. This document outlines the behavior of instances in a Versia federated network. {{ className: 'lead' }} Federation is built on the [HyperText Transfer Protocol (HTTP)](https://tools.ietf.org/html/rfc7230) and the [JavaScript Object Notation (JSON)](https://tools.ietf.org/html/rfc7159) data format. Instances communicate with each other by sending and receiving JSON payloads over HTTP. - - + + \ No newline at end of file diff --git a/app/federation/validation/page.mdx b/app/federation/validation/page.mdx index 69fb209..bc86f1d 100644 --- a/app/federation/validation/page.mdx +++ b/app/federation/validation/page.mdx @@ -1,12 +1,12 @@ export const metadata = { title: 'Validation', description: - 'Validation rules for Lysand implementations.', + 'Validation rules for Versia implementations.', } # Validation -Implementations **MUST** strictly validate all incoming data to ensure that it is well-formed and adheres to the Lysand Protocol. If a request is invalid, the server **MUST** return a `400 Bad Request` HTTP status code. +Implementations **MUST** strictly validate all incoming data to ensure that it is well-formed and adheres to the Versia Protocol. If a request is invalid, the server **MUST** return a `400 Bad Request` HTTP status code. Remember that while *your* implementation may disallow or restrict some user input, other implementations may not. You **should not** apply those restrictions to data coming from other instances. @@ -33,4 +33,4 @@ Things that should be validated include, but are not limited to: - The **validity** of all URLs and URIs (run them through your favorite URL parser, optionally fetch the linked URL). - The **time** of all dates and times (people should not be born in the future, or in the year 0). -It is your implementation's duty to reject data from other instances that does not adhere to the strict spec. **This is crucial to ensure the integrity of your instance and the network as a whole**. Allowing data that is technically valid but semantically incorrect can lead to the degradation of the entire Lysand ecosystem. +It is your implementation's duty to reject data from other instances that does not adhere to the strict spec. **This is crucial to ensure the integrity of your instance and the network as a whole**. Allowing data that is technically valid but semantically incorrect can lead to the degradation of the entire Versia ecosystem. diff --git a/app/introduction/page.mdx b/app/introduction/page.mdx index 251cc4f..65c9fbe 100644 --- a/app/introduction/page.mdx +++ b/app/introduction/page.mdx @@ -2,8 +2,8 @@ import { Resources } from '@/components/Resources' import { HeroPattern } from '@/components/HeroPattern' export const metadata = { - title: 'Lysand Documentation', - description: 'Introduction to the Lysand Protocol, a communication medium for federated applications, leveraging the HTTP stack.', + title: 'Versia Documentation', + description: 'Introduction to the Versia Protocol, a communication medium for federated applications, leveraging the HTTP stack.', } export const sections = [ @@ -14,9 +14,9 @@ export const sections = [ -# Lysand Federation Protocol +# Versia Federation Protocol -The Lysand Protocol is designed as a communication medium for federated applications, leveraging the HTTP stack. Its simplicity ensures ease of implementation and comprehension. {{ className: 'lead' }} +The Versia Protocol is designed as a communication medium for federated applications, leveraging the HTTP stack. Its simplicity ensures ease of implementation and comprehension. {{ className: 'lead' }}
    diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 046629b..f095e88 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -258,6 +258,7 @@ export const navigation: NavGroup[] = [ links: [ { title: "HTTP", href: "/federation/http" }, { title: "Validation", href: "/federation/validation" }, + { title: "Discovery", href: "/federation/discovery" }, ], }, { From 9f3bd64d7de839556fca77f0d7b4bb72877a5fe1 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 15:56:05 +0200 Subject: [PATCH 077/110] fix: :bug: Add missing new section to Federation docs --- app/federation/page.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/federation/page.mdx b/app/federation/page.mdx index c770bd0..b098fd5 100644 --- a/app/federation/page.mdx +++ b/app/federation/page.mdx @@ -15,4 +15,5 @@ Federation is built on the [HyperText Transfer Protocol (HTTP)](https://tools.ie + \ No newline at end of file From 8de6686a9a1ed05a3d2ef0b05ea7c6b9fe315c39 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:12:49 +0200 Subject: [PATCH 078/110] feat: :memo: Document ServerMetadata --- app/entities/server-metadata/page.mdx | 131 ++++++++++++++++++++++++++ app/entities/user/page.mdx | 2 +- app/federation/discovery/page.mdx | 8 +- components/Navigation.tsx | 1 + 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 app/entities/server-metadata/page.mdx diff --git a/app/entities/server-metadata/page.mdx b/app/entities/server-metadata/page.mdx new file mode 100644 index 0000000..189a689 --- /dev/null +++ b/app/entities/server-metadata/page.mdx @@ -0,0 +1,131 @@ +export const metadata = { + title: 'Server Metadata', + description: 'Metadata about a Versia instance, such as capabilities and endpoints.', +} + +# Server Metadata + +Contains metadata about a Versia instance, such as capabilities and endpoints. {{ className: 'lead' }} + +Not to be confused with [Server Actor](/entities/server-actor), which is a User representing an instance in the federation. + + + This entity is only used as part of [Server Discovery](/federation/discovery#server-discovery), and not as part of federation. + + +## Entity Definition + + + + + + This entity does not have an ID. + + + This entity does not have a URI. + + + Friendly name of the instance, for humans. + + + Information about the software running the instance. + + ```typescript + type Software = { + name: string; + version: string; + } + ``` + + - `name`: Name of the software. + - `version`: Version of the software. Should use [SemVer](https://semver.org/). + + + Information about the compatibility of the instance. + + ```typescript + type Compatibility = { + versions: string[]; + extensions: string[]; + } + ``` + + - `versions`: Supported Versia Protocol versions. + - Versions marked as "Working Draft X" are represented as `0.X`. + - `extensions`: Supported extensions. + + + Long description of the instance, for humans. Should be around 100-200 words. + + + Hostname of the instance. Includes the port if it is not the default (i.e. `443` for HTTPS). + + + URI to the instance's shared inbox, if supported. + + + URI to [Collection](/structures/collection) of instance moderators. + + + This is for human consumption (such as moderator contact), not for any kind of protocol authorization. + + + + URI to [Collection](/structures/collection) of instance administrators. + + + This is for human consumption (such as admin contact), not for any kind of protocol authorization. + + + + Logo of the instance. Must be an image format (`image/*`). + + + Banner of the instance. Must be an image format (`image/*`). + + + + + + + ```jsonc {{ 'title': 'ServerMetadata' }} + { + "type": "ServerMetadata", + "name": "Jim's Jolly Jimjams", + "software": { + "name": "Versia Server", + "version": "1.2.0-beta.3" + }, + "compatibility": { + "versions": [ + "0.3.0", + "0.4.0" + ], + "extensions": [ + "pub.versia:reactions", + "pub.versia:polls", + "pub.versia:reports" + ] + }, + "description": "Server for Jim's Jolly Jimjams, a social network for fans of Jimjams.", + "host": "social.jimjams.com", + "shared_inbox": "https://social.jimjams.com/inbox", + "moderators": "https://social.jimjams.com/moderators", + "admins": "https://social.jimjams.com/admins", + "logo": { + "image/png": { + "content": "https://social.jimjams.com/files/logo.png" + }, + "image/webp": { + "content": "https://social.jimjams.com/files/logo.webp" + } + }, + "banner": null, + "extensions": { + "example.extension:monthly_active_users": 1000 + } + } + ``` + + + \ No newline at end of file diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index ca7c319..a7bc18d 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -91,7 +91,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti The user's federation inbox. Refer to the [federation documentation](/federation). - Some instances may have a shared inbox, in which case that should be used instead. Refer to [Server Metadata](/entities/server-metadata) for more information. + Some instances may also have a shared inbox. Refer to [Server Metadata](/entities/server-metadata) for more information. Collections related to the user. Must contain at least `outbox`, `followers`, `following`, and `featured`. diff --git a/app/federation/discovery/page.mdx b/app/federation/discovery/page.mdx index 051278f..79aabb5 100644 --- a/app/federation/discovery/page.mdx +++ b/app/federation/discovery/page.mdx @@ -69,10 +69,10 @@ Accept: application/json "uri": "https://versia.social", "version": "3.2.0", "supported_extensions": [ - "org.lysand:reactions", - "org.lysand:polls", - "org.lysand:custom_emojis", - "org.lysand:is_cat" + "pub.versia:reactions", + "pub.versia:polls", + "pub.versia:custom_emojis", + "pub.versia:is_cat" ] } ``` \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index f095e88..dd8e860 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -277,6 +277,7 @@ export const navigation: NavGroup[] = [ { title: "FollowAccept", href: "/entities/follow-accept" }, { title: "FollowReject", href: "/entities/follow-reject" }, { title: "Notes", href: "/entities/note" }, + { title: "ServerMetadata", href: "/entities/server-metadata" }, { title: "Unfollow", href: "/entities/unfollow" }, { title: "Users", href: "/entities/user" }, ], From fe3a06d2c84346336def68301c465eeca5eb6037 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:13:42 +0200 Subject: [PATCH 079/110] docs: :memo: Update changelog --- app/changelog/page.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index c5e0ffd..c912127 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -29,4 +29,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Clarified the way [Follows](/entities/follow) work. - Removed the use of `Undo` entities for anything except than deleting entities. - Renamed `Undo` to [Delete](/entities/delete). -- Added [Unfollow](/entities/unfollow) entity. \ No newline at end of file +- Added [Unfollow](/entities/unfollow) entity. +- Completely rework [ServerMetadata](/entities/server-metadata). \ No newline at end of file From 9851b80f6d020c461fffc9af9b346fdcb21190f9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:14:41 +0200 Subject: [PATCH 080/110] chore: :arrow_up: Upgrade dependencies --- bun.lockb | Bin 193616 -> 194656 bytes package.json | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index bc8547b139cc844e1d4ef4f12b0573aeafed39a7..82df9e6650d5cd128ce431d228bef6cb346e61d7 100755 GIT binary patch delta 29794 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 T)-221aN+v;Z~5xJ;=KO@+REMp delta 29272 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

    #FkbdlKB%N9utw&8bk^&YYk#U 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 diff --git a/package.json b/package.json index f49538c..7d43caf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@sindresorhus/slugify": "^2.2.1", "@tailwindcss/typography": "^0.5.14", "@types/mdx": "^2.0.13", - "@types/node": "^22.2.0", + "@types/node": "^22.3.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-highlight-words": "^0.20.0", @@ -29,7 +29,7 @@ "clsx": "^2.1.1", "fast-glob": "^3.3.2", "flexsearch": "^0.7.43", - "framer-motion": "^11.3.24", + "framer-motion": "^11.3.27", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.4", "next": "^14.2.5", @@ -42,7 +42,7 @@ "remark-mdx": "^3.0.1", "shiki": "^1.12.1", "simple-functional-loader": "^1.2.1", - "tailwindcss": "^3.4.9", + "tailwindcss": "^3.4.10", "typescript": "^5.5.4", "unist-util-filter": "^5.0.1", "unist-util-visit": "^5.0.0", From c930bb0b3f14e6960828e0f93dbef6b45bf6001a Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:20:12 +0200 Subject: [PATCH 081/110] refactor: :fire: Remove useless feedback button --- components/mdx.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/mdx.tsx b/components/mdx.tsx index 0ceb89f..5b925a0 100644 --- a/components/mdx.tsx +++ b/components/mdx.tsx @@ -2,7 +2,6 @@ import clsx from "clsx"; import Link from "next/link"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; -import { Feedback } from "./Feedback"; import { Heading } from "./Heading"; import { Prose } from "./Prose"; @@ -15,9 +14,9 @@ export function wrapper({ children }: { children: ReactNode }) { return (

    {children} -
    + {/*
    -
    +
    */}
    ); } From 0ae85ccd424bbf86c6d82d1b2642fed3173c689f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:24:57 +0200 Subject: [PATCH 082/110] fix: :bug: Fix outdated ServerMetadata example --- app/federation/discovery/page.mdx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/federation/discovery/page.mdx b/app/federation/discovery/page.mdx index 79aabb5..80bcc1c 100644 --- a/app/federation/discovery/page.mdx +++ b/app/federation/discovery/page.mdx @@ -66,13 +66,20 @@ Accept: application/json { "type": "ServerMetadata", "name": "Versia Social", - "uri": "https://versia.social", - "version": "3.2.0", - "supported_extensions": [ - "pub.versia:reactions", - "pub.versia:polls", - "pub.versia:custom_emojis", - "pub.versia:is_cat" - ] + "software": { + "name": "Versia Server", + "version": "0.7.0" + }, + "compatibility": { + "versions": [ + "0.4.0" + ], + "extensions": [ + "pub.versia:reactions", + "pub.versia:polls", + "pub.versia:reports" + ] + }, + "host": "versia.social", } ``` \ No newline at end of file From c1e94ce1286c574dce55d879747b3a26d17ee7be Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 14 Aug 2024 16:32:44 +0200 Subject: [PATCH 083/110] fix: :bug: Correct some incorrect links --- app/extensions/websockets/page.mdx | 2 +- app/introduction/page.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/websockets/page.mdx b/app/extensions/websockets/page.mdx index 59b716d..28e7789 100644 --- a/app/extensions/websockets/page.mdx +++ b/app/extensions/websockets/page.mdx @@ -32,7 +32,7 @@ Messages sent over the WebSocket connection are JSON objects. Same as the `X-Signed-By` header in HTTP requests. - + Same as the request body in HTTP requests. Must be a string (stringified JSON), not JSON. diff --git a/app/introduction/page.mdx b/app/introduction/page.mdx index 65c9fbe..dc51f80 100644 --- a/app/introduction/page.mdx +++ b/app/introduction/page.mdx @@ -34,7 +34,7 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are us
    The Versia Protocol uses the following terms: -- **Entity**: A generic term for any JSON object in the protocol, such as an [Actor](./entities/actors), a [Note](./entities/note), or a [Like](./entities/likes). Entities are uniquely identified by their `id` property. +- **Entity**: A generic term for any JSON object in the protocol, such as a [User](./entities/user), a [Note](./entities/note), or a [Like](./extensions/likes). Entities are uniquely identified by their `id` property. - **Implementation**: A software application that implements the Versia Protocol. - **Instance**: An application deploying an **Implementation**. - Using the same nomenclature, an ActivityPub Implementation would be `Mastodon`, and an Instance would be `mastodon.social`. From c074e6e38e4732c34895af9dfdd1de7b1d57afa4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 17 Aug 2024 14:48:43 +0200 Subject: [PATCH 084/110] refactor: :fire: Remove Server Actors --- app/changelog/page.mdx | 3 ++- app/entities/delete/page.mdx | 6 +++--- app/entities/page.mdx | 2 +- app/entities/server-metadata/page.mdx | 20 ++++++++++++++++++-- app/federation/http/page.mdx | 2 +- app/signatures/page.mdx | 2 +- app/structures/collection/page.mdx | 4 ++-- 7 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index c912127..1129138 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -30,4 +30,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Removed the use of `Undo` entities for anything except than deleting entities. - Renamed `Undo` to [Delete](/entities/delete). - Added [Unfollow](/entities/unfollow) entity. -- Completely rework [ServerMetadata](/entities/server-metadata). \ No newline at end of file +- Completely rework [ServerMetadata](/entities/server-metadata). +- Remove Server Actors, and move instance public keys to [ServerMetadata](/entities/server-metadata). \ No newline at end of file diff --git a/app/entities/delete/page.mdx b/app/entities/delete/page.mdx index a40dc5f..864030c 100644 --- a/app/entities/delete/page.mdx +++ b/app/entities/delete/page.mdx @@ -13,7 +13,7 @@ Implementations **must** ensure that the author of the `Delete` entity has the a Having the authorization is defined as: - The author is the creator of the target entity (including [delegation](/delegation)). -- The author is the server actor. +- The author is the instance. ## Entity Definition @@ -23,8 +23,8 @@ Having the authorization is defined as: This entity does not have a URI. - - URI of the `User` who is deleting the entity. + + URI of the `User` who is deleting the entity. [Can be set to `null` to represent the instance](/entities/server-metadata#the-null-author). URI of the entity being deleted. diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 842b7d4..1ab2a30 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -82,4 +82,4 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. When serialized to a string, the JSON representation of an entity should follow the following rules: - Keys must be sorted lexicographically. - Should use UTF-8 encoding. -- Must be **signed** using the relevant [User](/entities/user)'s private key, or the [Server Actor](/entities/server-actor)'s private key if the entity is not associated with a particular user. +- Must be **signed** using the relevant [User](/entities/user)'s private key, or the [instance's private key](/entities/server-metadata) if the entity is not associated with a particular user. diff --git a/app/entities/server-metadata/page.mdx b/app/entities/server-metadata/page.mdx index 189a689..4ce3921 100644 --- a/app/entities/server-metadata/page.mdx +++ b/app/entities/server-metadata/page.mdx @@ -7,12 +7,14 @@ export const metadata = { Contains metadata about a Versia instance, such as capabilities and endpoints. {{ className: 'lead' }} -Not to be confused with [Server Actor](/entities/server-actor), which is a User representing an instance in the federation. - This entity is only used as part of [Server Discovery](/federation/discovery#server-discovery), and not as part of federation. +## The `null` Author + +On all entities that have an `author` field, the `author` can be `null` to represent the instance itself as the author (like ActivityPub's Server Actors). In this case, the instance's public key should be used to verify the entity. + ## Entity Definition @@ -63,6 +65,17 @@ Not to be confused with [Server Actor](/entities/server-actor), which is a User URI to the instance's shared inbox, if supported. + + Public key of the instance. + + ```typescript + type PublicKey = { + public_key: string; + } + ``` + + - `public_key`: Public key of the instance. Must follow the [Versia Public Key](/signatures) format. + URI to [Collection](/structures/collection) of instance moderators. @@ -120,6 +133,9 @@ Not to be confused with [Server Actor](/entities/server-actor), which is a User "content": "https://social.jimjams.com/files/logo.webp" } }, + "public_key": { + "public_key": "MCowBQYDK2VwAyEA9zhEMtQZetRl4QrLcz99i7jOa6ZVjX7aLfRUsMuKByI=" + }, "banner": null, "extensions": { "example.extension:monthly_active_users": 1000 diff --git a/app/federation/http/page.mdx b/app/federation/http/page.mdx index 54f6108..fe92d8e 100644 --- a/app/federation/http/page.mdx +++ b/app/federation/http/page.mdx @@ -8,7 +8,7 @@ export const metadata = { Versia uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. -ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [Server Actor](/entities/server-actor)'s private key. +ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [instance's private key](/entities/server-metadata). ## Requests diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx index ed0d0d6..f9d5b46 100644 --- a/app/signatures/page.mdx +++ b/app/signatures/page.mdx @@ -18,7 +18,7 @@ Versia uses cryptographic signatures to ensure the integrity and authenticity of A signature consists of a series of headers in an HTTP request. The following headers are used: - **`X-Signature`**: The signature itself, encoded in base64. -- **`X-Signed-By`**: URI of the user who signed the request. +- **`X-Signed-By`**: URI of the user who signed the request, [or the string `instance` to represent the instance](/entities/server-metadata#the-null-author). - **`X-Nonce`**: A random string generated by the client. This is used to prevent replay attacks. Signatures are **required on ALL federation traffic**. If a request does not have a signature, it **MUST** be rejected. Specifically, signatures must be put on: diff --git a/app/structures/collection/page.mdx b/app/structures/collection/page.mdx index a8a8210..d7ab268 100644 --- a/app/structures/collection/page.mdx +++ b/app/structures/collection/page.mdx @@ -18,8 +18,8 @@ Pages should be limited to a reasonable number of entities, such as 20 or 80. - - Author of the collection. Usually the user who owns the collection. Can be the server actor. + + Author of the collection. Usually the user who owns the collection. [Can be set to `null` to represent the instance](/entities/server-metadata#the-null-author). URI to the first page of the collection. Query parameters are allowed. From f62c2b3a19841f4b766af32f44da0d72b4551d85 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 17 Aug 2024 14:51:49 +0200 Subject: [PATCH 085/110] refactor: :truck: Rename ServerMetadata to InstanceMetadata --- app/changelog/page.mdx | 4 ++-- app/entities/{server-metadata => instance-metadata}/page.mdx | 4 ++-- app/federation/discovery/page.mdx | 4 ++-- components/Navigation.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename app/entities/{server-metadata => instance-metadata}/page.mdx (98%) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 1129138..6476ab8 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -30,5 +30,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Removed the use of `Undo` entities for anything except than deleting entities. - Renamed `Undo` to [Delete](/entities/delete). - Added [Unfollow](/entities/unfollow) entity. -- Completely rework [ServerMetadata](/entities/server-metadata). -- Remove Server Actors, and move instance public keys to [ServerMetadata](/entities/server-metadata). \ No newline at end of file +- Completely rework `ServerMetadata`, and rename to [InstanceMetadata](/entities/instance-metadata). +- Remove Server Actors, and move instance public keys to [InstanceMetadata](/entities/instance-metadata). \ No newline at end of file diff --git a/app/entities/server-metadata/page.mdx b/app/entities/instance-metadata/page.mdx similarity index 98% rename from app/entities/server-metadata/page.mdx rename to app/entities/instance-metadata/page.mdx index 4ce3921..ef2c4f8 100644 --- a/app/entities/server-metadata/page.mdx +++ b/app/entities/instance-metadata/page.mdx @@ -101,9 +101,9 @@ On all entities that have an `author` field, the `author` can be `null` to repre - ```jsonc {{ 'title': 'ServerMetadata' }} + ```jsonc {{ 'title': 'InstanceMetadata' }} { - "type": "ServerMetadata", + "type": "InstanceMetadata", "name": "Jim's Jolly Jimjams", "software": { "name": "Versia Server", diff --git a/app/federation/discovery/page.mdx b/app/federation/discovery/page.mdx index 80bcc1c..dc9441d 100644 --- a/app/federation/discovery/page.mdx +++ b/app/federation/discovery/page.mdx @@ -55,7 +55,7 @@ Server metadata can be accessed by making a `GET` request to the server's Versia To discover the metadata of the server `versia.social`, an instance would make a `GET` request to `https://versia.social/.well-known/versia`. -This endpoint will return a [ServerMetadata](/entities/server-metadata) entity. +This endpoint will return an [InstanceMetadata](/entities/instance-metadata) entity. ```http {{ 'title': 'Example Request' }} GET /.well-known/versia HTTP/1.1 @@ -64,7 +64,7 @@ Accept: application/json ```jsonc {{ 'title': 'Example Response' }} { - "type": "ServerMetadata", + "type": "InstanceMetadata", "name": "Versia Social", "software": { "name": "Versia Server", diff --git a/components/Navigation.tsx b/components/Navigation.tsx index dd8e860..52c0540 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -277,7 +277,7 @@ export const navigation: NavGroup[] = [ { title: "FollowAccept", href: "/entities/follow-accept" }, { title: "FollowReject", href: "/entities/follow-reject" }, { title: "Notes", href: "/entities/note" }, - { title: "ServerMetadata", href: "/entities/server-metadata" }, + { title: "InstanceMetadata", href: "/entities/server-metadata" }, { title: "Unfollow", href: "/entities/unfollow" }, { title: "Users", href: "/entities/user" }, ], From 290b6f96e9ca5b951a0d76506145f4270ecf6442 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 17 Aug 2024 14:58:41 +0200 Subject: [PATCH 086/110] refactor: :truck: Add algorithm, rename fields in public keys --- app/changelog/page.mdx | 4 +++- app/entities/instance-metadata/page.mdx | 11 +++++++---- app/entities/user/page.mdx | 8 ++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 6476ab8..3916255 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -31,4 +31,6 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Renamed `Undo` to [Delete](/entities/delete). - Added [Unfollow](/entities/unfollow) entity. - Completely rework `ServerMetadata`, and rename to [InstanceMetadata](/entities/instance-metadata). -- Remove Server Actors, and move instance public keys to [InstanceMetadata](/entities/instance-metadata). \ No newline at end of file +- Remove Server Actors, and move instance public keys to [InstanceMetadata](/entities/instance-metadata). +- Add `algorithm` to [Users](/entities/user) and [InstanceMetadata](/entities/instance-metadata)'s public keys for future use (only `ed25519` is allowed for now). + - Renamed the second `public_key` to `key`. \ No newline at end of file diff --git a/app/entities/instance-metadata/page.mdx b/app/entities/instance-metadata/page.mdx index ef2c4f8..21547ed 100644 --- a/app/entities/instance-metadata/page.mdx +++ b/app/entities/instance-metadata/page.mdx @@ -70,11 +70,13 @@ On all entities that have an `author` field, the `author` can be `null` to repre ```typescript type PublicKey = { - public_key: string; + algorithm: string; + key: string; } ``` - - - `public_key`: Public key of the instance. Must follow the [Versia Public Key](/signatures) format. + + - `algorithm`: Algorithm used for the public key. Can only be `ed25519` for now. + - `key`: Public key of the instance. Must follow the [Versia Public Key](/signatures) format. URI to [Collection](/structures/collection) of instance moderators. @@ -134,7 +136,8 @@ On all entities that have an `author` field, the `author` can be `null` to repre } }, "public_key": { - "public_key": "MCowBQYDK2VwAyEA9zhEMtQZetRl4QrLcz99i7jOa6ZVjX7aLfRUsMuKByI=" + "algorithm": "ed25519", + "key": "MCowBQYDK2VwAyEA9zhEMtQZetRl4QrLcz99i7jOa6ZVjX7aLfRUsMuKByI=" }, "banner": null, "extensions": { diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index a7bc18d..2f919b0 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -73,12 +73,15 @@ Instance **must** be the host of the instance the user is on (hostname with opti The user's public key. Must follow the [Versia Public Key](/signatures) format. `actor` may be a URI to another user's profile, in which case this key may allow the user to act on behalf of the other user (see [delegation](/delegation)). + `algorithm` must be `ed25519` for now. + ```typescript type URI = string; type PublicKey = { actor: URI; - public_key: string; + algorithm: string; + key: string; } ``` @@ -181,7 +184,8 @@ Instance **must** be the host of the instance the user is on (hostname with opti "manually_approves_followers": false, "public_key": { "actor": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771", - "public_key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + "algorithm": "ed25519", + "key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }, "username": "aprl" } From 94f437f53986cae5dc1d20223f4fd807b6419723 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 17 Aug 2024 22:16:59 +0200 Subject: [PATCH 087/110] fix: :bug: Finish InstanceMetadata rename --- app/entities/delete/page.mdx | 2 +- app/entities/page.mdx | 2 +- app/entities/user/page.mdx | 2 +- app/federation/http/page.mdx | 2 +- app/signatures/page.mdx | 2 +- app/structures/collection/page.mdx | 2 +- components/Navigation.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/entities/delete/page.mdx b/app/entities/delete/page.mdx index 864030c..f7ceae2 100644 --- a/app/entities/delete/page.mdx +++ b/app/entities/delete/page.mdx @@ -24,7 +24,7 @@ Having the authorization is defined as: This entity does not have a URI. - URI of the `User` who is deleting the entity. [Can be set to `null` to represent the instance](/entities/server-metadata#the-null-author). + URI of the `User` who is deleting the entity. [Can be set to `null` to represent the instance](/entities/instance-metadata#the-null-author). URI of the entity being deleted. diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 1ab2a30..a8b71f4 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -82,4 +82,4 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. When serialized to a string, the JSON representation of an entity should follow the following rules: - Keys must be sorted lexicographically. - Should use UTF-8 encoding. -- Must be **signed** using the relevant [User](/entities/user)'s private key, or the [instance's private key](/entities/server-metadata) if the entity is not associated with a particular user. +- Must be **signed** using the relevant [User](/entities/user)'s private key, or the [instance's private key](/entities/instance-metadata) if the entity is not associated with a particular user. diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index 2f919b0..c1a1ef1 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -94,7 +94,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti The user's federation inbox. Refer to the [federation documentation](/federation). - Some instances may also have a shared inbox. Refer to [Server Metadata](/entities/server-metadata) for more information. + Some instances may also have a shared inbox. Refer to [Server Metadata](/entities/instance-metadata) for more information. Collections related to the user. Must contain at least `outbox`, `followers`, `following`, and `featured`. diff --git a/app/federation/http/page.mdx b/app/federation/http/page.mdx index fe92d8e..19b5f3b 100644 --- a/app/federation/http/page.mdx +++ b/app/federation/http/page.mdx @@ -8,7 +8,7 @@ export const metadata = { Versia uses the HTTP protocol for all communications between instances. HTTP requests must conform to certain standards to ensure compatibility between different implementations, as well as to ensure the security and integrity of the data being exchanged. -ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [instance's private key](/entities/server-metadata). +ALL kinds of HTTP requests/responses between instances **MUST** include a [Signature](/signatures), signed with either the relevant [User](/entities/user)'s private key or the [instance's private key](/entities/instance-metadata). ## Requests diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx index f9d5b46..786cf72 100644 --- a/app/signatures/page.mdx +++ b/app/signatures/page.mdx @@ -18,7 +18,7 @@ Versia uses cryptographic signatures to ensure the integrity and authenticity of A signature consists of a series of headers in an HTTP request. The following headers are used: - **`X-Signature`**: The signature itself, encoded in base64. -- **`X-Signed-By`**: URI of the user who signed the request, [or the string `instance` to represent the instance](/entities/server-metadata#the-null-author). +- **`X-Signed-By`**: URI of the user who signed the request, [or the string `instance` to represent the instance](/entities/instance-metadata#the-null-author). - **`X-Nonce`**: A random string generated by the client. This is used to prevent replay attacks. Signatures are **required on ALL federation traffic**. If a request does not have a signature, it **MUST** be rejected. Specifically, signatures must be put on: diff --git a/app/structures/collection/page.mdx b/app/structures/collection/page.mdx index d7ab268..c78d358 100644 --- a/app/structures/collection/page.mdx +++ b/app/structures/collection/page.mdx @@ -19,7 +19,7 @@ Pages should be limited to a reasonable number of entities, such as 20 or 80. - Author of the collection. Usually the user who owns the collection. [Can be set to `null` to represent the instance](/entities/server-metadata#the-null-author). + Author of the collection. Usually the user who owns the collection. [Can be set to `null` to represent the instance](/entities/instance-metadata#the-null-author). URI to the first page of the collection. Query parameters are allowed. diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 52c0540..237cb9b 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -277,7 +277,7 @@ export const navigation: NavGroup[] = [ { title: "FollowAccept", href: "/entities/follow-accept" }, { title: "FollowReject", href: "/entities/follow-reject" }, { title: "Notes", href: "/entities/note" }, - { title: "InstanceMetadata", href: "/entities/server-metadata" }, + { title: "InstanceMetadata", href: "/entities/instance-metadata" }, { title: "Unfollow", href: "/entities/unfollow" }, { title: "Users", href: "/entities/user" }, ], From 1f864b89c856ea5f8a2822330cbb61a65f2316b4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 00:59:39 +0200 Subject: [PATCH 088/110] fix: :pencil2: Edit text for better consistency --- app/entities/instance-metadata/page.mdx | 8 ++------ app/entities/note/page.mdx | 4 ++-- app/entities/user/page.mdx | 2 +- app/federation/discovery/page.mdx | 6 +++--- app/federation/validation/page.mdx | 2 +- app/page.tsx | 2 +- app/signatures/page.mdx | 8 ++++---- 7 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/entities/instance-metadata/page.mdx b/app/entities/instance-metadata/page.mdx index 21547ed..78b9dac 100644 --- a/app/entities/instance-metadata/page.mdx +++ b/app/entities/instance-metadata/page.mdx @@ -1,16 +1,12 @@ export const metadata = { - title: 'Server Metadata', + title: 'Instance Metadata', description: 'Metadata about a Versia instance, such as capabilities and endpoints.', } -# Server Metadata +# Instance Metadata Contains metadata about a Versia instance, such as capabilities and endpoints. {{ className: 'lead' }} - - This entity is only used as part of [Server Discovery](/federation/discovery#server-discovery), and not as part of federation. - - ## The `null` Author On all entities that have an `author` field, the `author` can be `null` to represent the instance itself as the author (like ActivityPub's Server Actors). In this case, the instance's public key should be used to verify the entity. diff --git a/app/entities/note/page.mdx b/app/entities/note/page.mdx index c35a6a5..dd5908a 100644 --- a/app/entities/note/page.mdx +++ b/app/entities/note/page.mdx @@ -64,7 +64,7 @@ Notes represent a piece of content on a Versia instance. They can be posted by [ URIs of [Users](/entities/user) that should be notified of the note. Similar to Twitter's `@` mentions. The note may also contain mentions in the content, however only the mentions in this field should trigger notifications. - Previews for any links in the publication. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every server that sees the publication, creating an accidental DDOS attack. + Previews for any links in the publication. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every instance that sees the publication, creating an accidental DDOS attack. ```typescript type LinkPreview = { @@ -77,7 +77,7 @@ Notes represent a piece of content on a Versia instance. They can be posted by [ ``` - Servers should make sure not to trust the previews, as they could be faked by malicious remote servers. This is not a very good attack vector, but it is still possible to redirect users to malicious links. + Implementations should make sure not to trust the previews, as they could be faked by malicious remote instances. This is not a very good attack vector, but it is still possible to redirect users to malicious links. diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index c1a1ef1..cd3861e 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -94,7 +94,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti The user's federation inbox. Refer to the [federation documentation](/federation). - Some instances may also have a shared inbox. Refer to [Server Metadata](/entities/instance-metadata) for more information. + Some instances may also have a shared inbox. Refer to [Instance Metadata](/entities/instance-metadata) for more information. Collections related to the user. Must contain at least `outbox`, `followers`, `following`, and `featured`. diff --git a/app/federation/discovery/page.mdx b/app/federation/discovery/page.mdx index dc9441d..fe170b9 100644 --- a/app/federation/discovery/page.mdx +++ b/app/federation/discovery/page.mdx @@ -47,13 +47,13 @@ Accept: application/jrd+json } ``` -## Server Discovery +## Instance Discovery -Server metadata can be accessed by making a `GET` request to the server's Versia metadata endpoint, which is located at `/.well-known/versia`. +Instaance metadata can be accessed by making a `GET` request to the instance's Versia metadata endpoint, which is located at `/.well-known/versia`. ### Example -To discover the metadata of the server `versia.social`, an instance would make a `GET` request to `https://versia.social/.well-known/versia`. +To discover the metadata of the instance `versia.social`, an instance would make a `GET` request to `https://versia.social/.well-known/versia`. This endpoint will return an [InstanceMetadata](/entities/instance-metadata) entity. diff --git a/app/federation/validation/page.mdx b/app/federation/validation/page.mdx index bc86f1d..a980085 100644 --- a/app/federation/validation/page.mdx +++ b/app/federation/validation/page.mdx @@ -6,7 +6,7 @@ export const metadata = { # Validation -Implementations **MUST** strictly validate all incoming data to ensure that it is well-formed and adheres to the Versia Protocol. If a request is invalid, the server **MUST** return a `400 Bad Request` HTTP status code. +Implementations **MUST** strictly validate all incoming data to ensure that it is well-formed and adheres to the Versia Protocol. If a request is invalid, the instance **MUST** return a `400 Bad Request` HTTP status code. Remember that while *your* implementation may disallow or restrict some user input, other implementations may not. You **should not** apply those restrictions to data coming from other instances. diff --git a/app/page.tsx b/app/page.tsx index 30061fb..28183ef 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -50,7 +50,7 @@ const Page: FC = () => { { name: "In-depth security docs", description: - "Docs provide lots of information on how to program a secure server.", + "Docs provide lots of information on how to program a secure instance.", icon: "tabler:shield", }, { diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx index 786cf72..2b2c204 100644 --- a/app/signatures/page.mdx +++ b/app/signatures/page.mdx @@ -1,7 +1,7 @@ export const metadata = { title: 'Signatures', description: - 'Learn how signatures work, and how to implement them in your Versia server.', + 'Learn how signatures work, and how to implement them in your Versia instance.', } # Signatures @@ -9,7 +9,7 @@ export const metadata = { Versia uses cryptographic signatures to ensure the integrity and authenticity of data. Signatures are used to verify that the data has not been tampered with and that it was created by the expected user. {{ className: 'lead' }} - This part is very important! If signatures are implemented incorrectly in your server, **you will not be able to federate**. + This part is very important! If signatures are implemented incorrectly in your instance, **you will not be able to federate**. Mistakes made in this section can lead to **security vulnerabilities** and **impersonation attacks**. @@ -25,7 +25,7 @@ Signatures are **required on ALL federation traffic**. If a request does not hav - **All POST requests**. - **All responses to GET requests** (for example, when fetching a user's profile). In this case, the HTTP method used in the signature string must be `GET`. -If a signature fails, is missing or is invalid, the server **MUST** return a `401 Unauthorized` HTTP status code. +If a signature fails, is missing or is invalid, the instance **MUST** return a `401 Unauthorized` HTTP status code. ### Calculating the Signature @@ -49,7 +49,7 @@ post /notes a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 n4b ### Verifying the Signature -To verify a signature, the server must: +To verify a signature, the instance must: - Recreate the string as described above. - Extract the signature provided in the `X-Signature` header. - Decode the signature from base64. From c8cba54a72e4f15f1ce0c096326c9fa13e9b3ad1 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 13:34:05 +0200 Subject: [PATCH 089/110] feat: :sparkles: Add Group --- app/entities/group/page.mdx | 58 +++++++++++++++++++++++++++++++++++++ app/entities/note/page.mdx | 2 +- components/Navigation.tsx | 1 + 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 app/entities/group/page.mdx diff --git a/app/entities/group/page.mdx b/app/entities/group/page.mdx new file mode 100644 index 0000000..ec9e4cc --- /dev/null +++ b/app/entities/group/page.mdx @@ -0,0 +1,58 @@ +export const metadata = { + title: 'Groups', + description: 'Groups are a way to organize users and notes into communities.' +} + +# Groups + +Groups are a way to organize users and notes into communities. They can be used for any purpose, such as forums, blogs, image galleries, video sharing, audio sharing, and messaging. They are similar to Discord's channels or Matrix's rooms. {{ className: 'lead' }} + +Refer to [Note](/entities/note#entity-definition)'s `group` property for how notes can be associated with groups. + +## Entity Definition + + + + + + Group name/title. + + Text only (`text/plain`, `text/html`, etc). + + + Short description of the group's contents and purpose. + + Text only (`text/plain`, `text/html`, etc). + + + URI of the group's members list. [Collection](/structures/collection) of [Users](/entities/user). + + + URI of the group's associated notes. [Collection](/structures/collection) of [Notes](/entities/note). + + + + + + + ```jsonc {{ title: "Example Group" }} + { + "type": "Group", + "id": "ed480922-b095-4f09-9da5-c995be8f5960", + "uri": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960", + "name": { + "text/html": { + "content": "The Woozy fan club" + } + }, + "description": { + "text/plain": { + "content": "A group for fans of the Woozy emoji." + } + }, + "members": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960/members", + } + ``` + + + \ No newline at end of file diff --git a/app/entities/note/page.mdx b/app/entities/note/page.mdx index dd5908a..4b9c311 100644 --- a/app/entities/note/page.mdx +++ b/app/entities/note/page.mdx @@ -51,7 +51,7 @@ Notes represent a piece of content on a Versia instance. They can be posted by [ ``` - URI of a [Group](/groups) that the note is only visible in, or one of the following strings: + URI of a [Group](/entities/group) that the note is only visible in, or one of the following strings: - `public`: The note is visible to anyone. - `followers`: The note is visible only to the author's followers. diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 237cb9b..963106a 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -276,6 +276,7 @@ export const navigation: NavGroup[] = [ { title: "Follow", href: "/entities/follow" }, { title: "FollowAccept", href: "/entities/follow-accept" }, { title: "FollowReject", href: "/entities/follow-reject" }, + { title: "Group", href: "/entities/group" }, { title: "Notes", href: "/entities/note" }, { title: "InstanceMetadata", href: "/entities/instance-metadata" }, { title: "Unfollow", href: "/entities/unfollow" }, From fb197167c7af8622e9de386883d2f7e71722f83b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 15:10:45 +0200 Subject: [PATCH 090/110] feat: :sparkles: Document Extension --- app/extensions/page.mdx | 122 ++++++++++++++++++++++++++++++++++++++ components/Navigation.tsx | 1 + 2 files changed, 123 insertions(+) create mode 100644 app/extensions/page.mdx diff --git a/app/extensions/page.mdx b/app/extensions/page.mdx new file mode 100644 index 0000000..82583cc --- /dev/null +++ b/app/extensions/page.mdx @@ -0,0 +1,122 @@ +export const metadata = { + title: "Extensions", + description: "Extensions are a way to add custom functionality to Versia." +} + +# Extensions + +Versia provides a set of core entities and structures to build a barebones social network. However, it is not possible nor desirable to cover every use case. This is where extensions come in, allowing parts of the system to be extended or replaced with custom functionality. {{ className: 'lead' }} + +By design, extensions can be mitchmatched in any combination, without requiring any changes to the core system. This allows for a high degree of customization and flexibility. Implementations that do not support a particular extension can simply ignore it without any issues. + +Extensions **should** be standardized and publicly documented. + +## Handling Unsupported Extensions + +When an extension is not supported by an Implementation, it **can** be ignored. This means that the extension is not processed, and its data is not used. Implementations **must not** throw an error when encountering an unsupported extension, as long as the rest of the entity is valid. + +Extensions **must not** be designed in a way that makes them required to understand or process other non-extension entities. + +## Naming + +Versia extension names are composed of two parts: +- The domain name of the extension author, in reverse order. Example: `pub.versia` +- The extension name, separated by a colon. Example: `likes` + +``` {{ title: "Example Extension Name" }} +pub.versia:likes +``` + +### Custom entities + +Custom entities are named in the same way, but with an additional part: +- The entity name, separated by a slash. PascalCase. Example: `Like` + +``` {{ title: "Example Custom Entity Type" }} +pub.versia:likes/Like +``` + +## Extension Definition + +Extensions can be found in two places: an [Entity](/entities#entity-definition)'s `extensions` property, or as custom entities themselves. The former is used to add custom functionality to an existing entity, while the latter is used to define a new entity type. + +### Entity Extension + + + + + + Custom extensions to the entity. + + - `key`: The extension name. + - `value`: Extension data. Can be any JSON-serializable data. + + Extension data can be any JSON-serializable data. + + + + + + ```jsonc {{ title: "Example Entity Extension" }} + { + "type": "Group", + "id": "ed480922-b095-4f09-9da5-c995be8f5960", + "uri": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960", + "name": null, + "description": null, + "members": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960/members", + "extensions": { // [!code focus:100] + "com.example:gps": { + "location": { + "latitude": 37.7749, + "longitude": -122.4194 + }, + "accuracy": 10, + "name": "San Francisco" + } + } + } + ``` + + + + +### Custom Entity + + + + + + The entity type. **Must** be `Extension`. The extension type is defined in the `extension_type` property. + + + The extension type. [Must follow naming conventions](#naming). + + + Other properties of the custom entity. These are specific to the extension, and should be documented by the extension author. + + Note that `id`, `uri` and `created_at` are still required for custom entities, unless the extension author specifies otherwise. + + + + + + ```jsonc {{ title: "Example Custom Entity" }} + { + "type": "Extension", + "extension_type": "com.example:poll/Poll", + "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "created_at": "2021-01-01T00:00:00.000Z", + "question": "What is your favourite colour?", + "options": [ + "Red", + "Blue", + "Green" + ] + } + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 963106a..522dae7 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -251,6 +251,7 @@ export const navigation: NavGroup[] = [ { title: "Entities", href: "/entities" }, { title: "Signatures", href: "/signatures" }, { title: "Federation", href: "/federation" }, + { title: "Extensions", href: "/extensions" }, ], }, { From 64cf9d350903a3c948736eace884c0673d8117ab Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 16:11:45 +0200 Subject: [PATCH 091/110] feat: :sparkles: Add Custom Emojis extension --- app/changelog/page.mdx | 2 +- app/extensions/custom-emojis/page.mdx | 79 +++++++++++++++++++++++++++ app/extensions/page.mdx | 4 +- app/structures/emoji/page.mdx | 4 +- components/Navigation.tsx | 1 + 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 app/extensions/custom-emojis/page.mdx diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 3916255..b350707 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -22,7 +22,7 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Added shared inboxes. - Added `remote` field to [ContentFormat](/structures/content-format). - Switched to [ThumbHash](https://evanw.github.io/thumbhash/) from [BlurHash](https://blurha.sh/). -- Added optional identification characters to [Custom Emojis](/structures/emoji). +- Added identification characters to [Custom Emojis](/structures/emoji). - Added `manually_approves_followers` to [Users](/entities/user). - Removed `visibility` from [Notes](/entities/note). - Made `subject` optional in [Notes](/entities/note). diff --git a/app/extensions/custom-emojis/page.mdx b/app/extensions/custom-emojis/page.mdx new file mode 100644 index 0000000..867a166 --- /dev/null +++ b/app/extensions/custom-emojis/page.mdx @@ -0,0 +1,79 @@ +export const metadata = { + title: "Custom Emojis Extension", + description: "The Custom Emojis extension adds support for custom emojis in notes.", +} + +# Custom Emojis Extension + +The Custom Emojis extension adds support for adding personalized emojis to federated data. {{ className: 'lead' }} + +## Rendering + +To render custom emojis, clients **should** perform the following steps: +1. Find all instances of custom emoji names in the text content. +2. Replace the custom emoji names with the corresponding emoji images in text. + +If custom emojis are not supported, clients **should** leave the custom emoji names as-is. Images **should** have any associated `alt` text for accessibility. + +```html {{ 'title': 'Example HTML/CSS' }} + + +

    + Hello, world! A happy emoji smiling.! +

    +``` + +Emojis **should** be displayed at a fixed height (such as `1em`), but their width **should** be allowed to be flexible. + +## Extension Definition + +Custom Emojis can be added to any entity with text content. The extension ID is `pub.versia:custom_emojis`. + + + + + + [Custom emojis](/structures/emoji) to be added to the note. + + + + + + + ```jsonc {{ 'title': 'Example Usage' }} + { + "id": "456df8ed-daf1-4062-abab-491071c7b8dd", + "type": "Note", + "uri": "https://versia.social/notes/456df8ed-daf1-4062-abab-491071c7b8dd", + "created_at": "2024-04-09T01:38:51.743Z", + "content": { + "text/plain": { + "content": "Hello, world :happy_face:!" + } + }, + "extensions": { // [!code focus:16] + "pub.versia:custom_emojis": { + "emojis": [ + { + "name": ":happy_face:", + "content": { + "image/webp": { + "content": "https://cdn.example.com/emojis/happy_face.webp", + "remote": true, + "description": "A happy emoji smiling.", + } + } + } + ] + } + } + } + ``` + + + \ No newline at end of file diff --git a/app/extensions/page.mdx b/app/extensions/page.mdx index 82583cc..05cc9d4 100644 --- a/app/extensions/page.mdx +++ b/app/extensions/page.mdx @@ -21,7 +21,7 @@ Extensions **must not** be designed in a way that makes them required to underst Versia extension names are composed of two parts: - The domain name of the extension author, in reverse order. Example: `pub.versia` -- The extension name, separated by a colon. Example: `likes` +- The extension name, separated by a colon. `snake_case`. Example: `likes` ``` {{ title: "Example Extension Name" }} pub.versia:likes @@ -30,7 +30,7 @@ pub.versia:likes ### Custom entities Custom entities are named in the same way, but with an additional part: -- The entity name, separated by a slash. PascalCase. Example: `Like` +- The entity name, separated by a slash. `PascalCase`. Example: `Like` ``` {{ title: "Example Custom Entity Type" }} pub.versia:likes/Like diff --git a/app/structures/emoji/page.mdx b/app/structures/emoji/page.mdx index f3ebeb1..078c8cf 100644 --- a/app/structures/emoji/page.mdx +++ b/app/structures/emoji/page.mdx @@ -17,11 +17,11 @@ export const metadata = { - Emoji name, optionally surrounded by identification characters (for example, colons: `:happy_face:`). + Emoji name, surrounded by identification characters (for example, colons: `:happy_face:`). Name must match the regex `^[a-zA-Z0-9_-]+$`. - Identification characters must not match the name regex (must not be alphanumeric/underscore/hyphen). + Identification characters must not match the name regex (must not be alphanumeric/underscore/hyphen). There may only be two identification characters, one at the beginning and one at the end. Emoji content. Must be an image format (`image/*`). diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 522dae7..46cbd65 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -287,6 +287,7 @@ export const navigation: NavGroup[] = [ { title: "Extensions", links: [ + { title: "Custom Emojis", href: "/extensions/custom-emojis" }, { title: "Likes", href: "/extensions/likes" }, { title: "Vanity", href: "/extensions/vanity" }, { title: "WebSockets", href: "/extensions/websockets" }, From c9b6ef8011a285712935d2b3f9ffbf493356a64c Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 17:34:17 +0200 Subject: [PATCH 092/110] feat: :sparkles: Add Share extension --- app/extensions/share/page.mdx | 48 +++++++++++++++++++++++++++++++++++ components/Navigation.tsx | 1 + 2 files changed, 49 insertions(+) create mode 100644 app/extensions/share/page.mdx diff --git a/app/extensions/share/page.mdx b/app/extensions/share/page.mdx new file mode 100644 index 0000000..003d7f3 --- /dev/null +++ b/app/extensions/share/page.mdx @@ -0,0 +1,48 @@ +export const metadata = { + title: "Share Extension", + description: "Share Extension lets users share notes they like with others.", +} + +# Share Extension + +The Share Extension lets users share notes they like with others. This is the same as Twitter's "retweet" and Mastodon's "boost". {{ className: 'lead' }} + +## Behaviour + +When a user shares a note, the note's original author **must** receive the entity alongside the user's followers. In clients, `Shares` should be rendered in a way that makes it clear that the shared note was originally authored by someone else than the user who shared it. + +`Shares` can be undone ("unboosting") with a [Delete](/entities/delete) entity. + +## Entity Definition + + + + + + Must be `pub.versia:share/Share`. + + + Creator of the Share. + + + URI of the note being shared. Must link to a [Note](/entities/note). + + + + + + + ```jsonc {{ 'title': 'Example Share' }} + { + "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", + "type": "Extension", + "extension_type": "pub.versia:share/Share", + "created_at": "2021-01-01T00:00:00.000Z", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "uri": "https://example.com/shares/3e7e4750-afd4-4d99-a256-02f0710a0520", + "shared": "https://otherexample.org/notes/fmKZ763jzIU8" + } + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 46cbd65..a9d00c9 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -289,6 +289,7 @@ export const navigation: NavGroup[] = [ links: [ { title: "Custom Emojis", href: "/extensions/custom-emojis" }, { title: "Likes", href: "/extensions/likes" }, + { title: "Share", href: "/extensions/share" }, { title: "Vanity", href: "/extensions/vanity" }, { title: "WebSockets", href: "/extensions/websockets" }, ], From 5cd5e4246fef263bef1d16a4940938219a2b070b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 17:35:25 +0200 Subject: [PATCH 093/110] chore: :memo: Update changelog --- app/changelog/page.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index b350707..6f0396a 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -33,4 +33,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Completely rework `ServerMetadata`, and rename to [InstanceMetadata](/entities/instance-metadata). - Remove Server Actors, and move instance public keys to [InstanceMetadata](/entities/instance-metadata). - Add `algorithm` to [Users](/entities/user) and [InstanceMetadata](/entities/instance-metadata)'s public keys for future use (only `ed25519` is allowed for now). - - Renamed the second `public_key` to `key`. \ No newline at end of file + - Renamed the second `public_key` to `key`. +- Renamed `Announce` to [Share](/extensions/share). \ No newline at end of file From b2ce198d4311d6b0ac079aa25301c950d9ab798f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 18 Aug 2024 17:36:26 +0200 Subject: [PATCH 094/110] chore: :arrow_up: Upgrade dependencies --- bun.lockb | Bin 194656 -> 194656 bytes package.json | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bun.lockb b/bun.lockb index 82df9e6650d5cd128ce431d228bef6cb346e61d7..46171157aae31b675abe5095693468890f78777c 100755 GIT binary patch delta 3342 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 DGNB#+ delta 3345 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 diff --git a/package.json b/package.json index 7d43caf..ec0292a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@sindresorhus/slugify": "^2.2.1", "@tailwindcss/typography": "^0.5.14", "@types/mdx": "^2.0.13", - "@types/node": "^22.3.0", + "@types/node": "^22.4.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-highlight-words": "^0.20.0", @@ -29,7 +29,7 @@ "clsx": "^2.1.1", "fast-glob": "^3.3.2", "flexsearch": "^0.7.43", - "framer-motion": "^11.3.27", + "framer-motion": "^11.3.28", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.4", "next": "^14.2.5", @@ -40,20 +40,20 @@ "remark": "^15.0.1", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", - "shiki": "^1.12.1", + "shiki": "^1.14.1", "simple-functional-loader": "^1.2.1", "tailwindcss": "^3.4.10", "typescript": "^5.5.4", "unist-util-filter": "^5.0.1", "unist-util-visit": "^5.0.0", - "zustand": "^4.5.4" + "zustand": "^4.5.5" }, "devDependencies": { "@biomejs/biome": "^1.8.3", "@iconify-icon/react": "^2.1.0", "@next/bundle-analyzer": "^14.2.5", - "@shikijs/transformers": "^1.12.1", - "sharp": "^0.33.4" + "@shikijs/transformers": "^1.14.1", + "sharp": "^0.33.5" }, "trustedDependencies": ["@biomejs/biome", "sharp"] } From 2845fd01fc2b565ee903349787b18030c7cae97d Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 22 Aug 2024 21:58:16 +0200 Subject: [PATCH 095/110] feat: :sparkles: Make Deletes also contain a deleted_type attribute --- app/entities/delete/page.mdx | 4 ++++ app/entities/page.mdx | 1 + 2 files changed, 5 insertions(+) diff --git a/app/entities/delete/page.mdx b/app/entities/delete/page.mdx index f7ceae2..e9823b5 100644 --- a/app/entities/delete/page.mdx +++ b/app/entities/delete/page.mdx @@ -26,6 +26,9 @@ Having the authorization is defined as: URI of the `User` who is deleting the entity. [Can be set to `null` to represent the instance](/entities/instance-metadata#the-null-author). + + Type of the entity being deleted. In case of extensions, use the entity's `extension_type`. + URI of the entity being deleted. @@ -40,6 +43,7 @@ Having the authorization is defined as: "id": "9b3212b8-529c-435a-8798-09ebbc17ca74", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", + "deleted_type": "Note", "deleted": "https://example.com/notes/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7" } ``` diff --git a/app/entities/page.mdx b/app/entities/page.mdx index a8b71f4..0e99332 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -51,6 +51,7 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. "type": "Delete", "created_at": "2022-01-01T12:00:00Z", "author": "https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e", + "deleted_type": "Note", "deleted": "https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b", } ``` From aeca63f90d7a9bdf3b26e7414415f5951c55e8d8 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 22 Aug 2024 21:58:59 +0200 Subject: [PATCH 096/110] fix: :pencil2: Correct typo --- app/entities/follow/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/entities/follow/page.mdx b/app/entities/follow/page.mdx index d5aae8a..6563f26 100644 --- a/app/entities/follow/page.mdx +++ b/app/entities/follow/page.mdx @@ -5,7 +5,7 @@ export const metadata = { # Follow -Sometimes, [Users](/entities/user) want to subscribe to each other to see each other's content. The `Follow` entity facilitates this, by definining a subscription relationship between two users. {{ className: 'lead' }} +Sometimes, [Users](/entities/user) want to subscribe to each other to see each other's content. The `Follow` entity facilitates this, by defining a subscription relationship between two users. {{ className: 'lead' }} ## Vocabulary From 591adb7359ce9f9e8cf60d76b054c6f52c5c02fd Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 22 Aug 2024 22:06:31 +0200 Subject: [PATCH 097/110] fix: :bug: Add missing wording about database edits --- app/entities/follow/page.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/entities/follow/page.mdx b/app/entities/follow/page.mdx index 6563f26..179200b 100644 --- a/app/entities/follow/page.mdx +++ b/app/entities/follow/page.mdx @@ -33,6 +33,7 @@ To accept the follow request, `Alice` can do the following: 1. Create a [FollowAccept](/entities/follow-accept) entity with `Alice` as the author and `Joe` as the follower. 2. Send the `FollowAccept` entity to `Joe`'s inbox. +3. Update the relationship status in its database to "accepted". ### Rejecting the Follow Request @@ -40,6 +41,7 @@ To reject the follow request, `Alice` can do the following: 1. Create a [FollowReject](/entities/follow-reject) entity with `Alice` as the author and `Joe` as the follower. 2. Send the `FollowReject` entity to `Joe`'s inbox. +3. Optionally, log the rejection in its database. ### Final Steps From e75eb6d2f59be7fb948c514fd1ef3b0b4718d4ad Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 23 Aug 2024 08:52:03 +0200 Subject: [PATCH 098/110] fix: :truck: Rename prev to previous --- app/changelog/page.mdx | 3 ++- app/structures/collection/page.mdx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 6f0396a..bb7fbc4 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -34,4 +34,5 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Remove Server Actors, and move instance public keys to [InstanceMetadata](/entities/instance-metadata). - Add `algorithm` to [Users](/entities/user) and [InstanceMetadata](/entities/instance-metadata)'s public keys for future use (only `ed25519` is allowed for now). - Renamed the second `public_key` to `key`. -- Renamed `Announce` to [Share](/extensions/share). \ No newline at end of file +- Renamed `Announce` to [Share](/extensions/share). +- Renamed `prev` to `previous` in [Collections](/structures/collection). \ No newline at end of file diff --git a/app/structures/collection/page.mdx b/app/structures/collection/page.mdx index c78d358..12ee97f 100644 --- a/app/structures/collection/page.mdx +++ b/app/structures/collection/page.mdx @@ -37,7 +37,7 @@ Pages should be limited to a reasonable number of entities, such as 20 or 80. If there is no next page, this should be `null`. - + URI to the previous page of the collection. Query parameters are allowed. @@ -58,7 +58,7 @@ Pages should be limited to a reasonable number of entities, such as 20 or 80. "last": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/outbox?page=3", "total": 46, "next": "https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771/outbox?page=2", - "prev": null, + "previous": null, "items": [ { "id": "456df8ed-daf1-4062-abab-491071c7b8dd", From 04b03e136b8e30c666222fb9fc3e3b060d18a8e7 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 23 Aug 2024 16:43:37 +0200 Subject: [PATCH 099/110] feat: :sparkles: Add Delegation --- app/changelog/page.mdx | 1 + app/entities/delete/page.mdx | 2 +- app/entities/instance-metadata/page.mdx | 2 +- app/entities/user/page.mdx | 6 ++++-- app/federation/delegation/page.mdx | 25 +++++++++++++++++++++++++ app/federation/page.mdx | 1 + components/Navigation.tsx | 1 + 7 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 app/federation/delegation/page.mdx diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index bb7fbc4..94a207b 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -12,6 +12,7 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Rewrote the signature system from scratch to be simpler and not depend on dates. - Moved Likes and Dislikes to an extension. +- Added [Delegation](/federation/delegation). - Renamed fields on several common entities like [Users](/entities/user) and [Notes](/entities/note). - Removed the `Patch` entity. - Useless since edits can just be sent to inboxes directly. diff --git a/app/entities/delete/page.mdx b/app/entities/delete/page.mdx index e9823b5..5495431 100644 --- a/app/entities/delete/page.mdx +++ b/app/entities/delete/page.mdx @@ -12,7 +12,7 @@ Signals the deletion of an entity. {{ className: 'lead' }} Implementations **must** ensure that the author of the `Delete` entity has the authorization to delete the target entity. Having the authorization is defined as: -- The author is the creator of the target entity (including [delegation](/delegation)). +- The author is the creator of the target entity (including [delegation](/federation/delegation)). - The author is the instance. ## Entity Definition diff --git a/app/entities/instance-metadata/page.mdx b/app/entities/instance-metadata/page.mdx index 78b9dac..79d19b0 100644 --- a/app/entities/instance-metadata/page.mdx +++ b/app/entities/instance-metadata/page.mdx @@ -72,7 +72,7 @@ On all entities that have an `author` field, the `author` can be `null` to repre ``` - `algorithm`: Algorithm used for the public key. Can only be `ed25519` for now. - - `key`: Public key of the instance. Must follow the [Versia Public Key](/signatures) format. + - `key`: Instance public key, in SPKI-encoded base64 (from raw bytes, not a PEM format). URI to [Collection](/structures/collection) of instance moderators. diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index cd3861e..22afa3a 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -71,9 +71,11 @@ Instance **must** be the host of the instance the user is on (hostname with opti A header image for the user's profile. Also known as a cover photo or a banner. Must be an image format (`image/*`). - The user's public key. Must follow the [Versia Public Key](/signatures) format. `actor` may be a URI to another user's profile, in which case this key may allow the user to act on behalf of the other user (see [delegation](/delegation)). + The user's public key. Must follow the [Versia Public Key](/signatures) format. `actor` may be a URI to another user's profile, in which case this key may allow the other user act on behalf of this user (see [delegation](/federation/delegation)). - `algorithm` must be `ed25519` for now. + - `algorithm`: Must be `ed25519` for now. + - `key`: The public key in SPKI-encoded base64 (from raw bytes, not a PEM format). Must be the key associated with the `actor` URI. + - `actor`: URI to a user's profile, most often the user's own profile. ```typescript type URI = string; diff --git a/app/federation/delegation/page.mdx b/app/federation/delegation/page.mdx new file mode 100644 index 0000000..43d4e8c --- /dev/null +++ b/app/federation/delegation/page.mdx @@ -0,0 +1,25 @@ +export const metadata = { + title: 'Delegation', + description: 'Delegation is used to authorize actions on behalf of another user', +} + +# Delegation + +Delegation is used to authorize actions on behalf of another user. {{ className: 'lead' }} + +## Vocabulary + +- **Delegator**: The user that is delegating actions to another user. (The user that owns the key) +- **Delegate**: The user that is being delegated actions. (The user that the key is pointing to) + +## The `actor` Field on Public Keys + +[Users](/entities/user)'s `public_key` property contains a field called `actor`. This field contains the URI to the **delegator** user, which is used to authorize actions on behalf of the **delegate** user. + +This means that the **delegator** user can sign requests with their private key, and any implementations should consider the **delegate** user as equivalent to the **delegator** user. + +## Implementation Details + +Any actions or entities created by the **delegate** should be attributed to the **delegator** user in clients transparently to end-users (e.g. showing the **delegator** user's name and avatar). This allows for a form of "consensual impersonation" that is authorized by the **delegators** and **delegates**. + +This is useful as a way to centralize all of a user's many "alt accounts" into a single, unified feed. \ No newline at end of file diff --git a/app/federation/page.mdx b/app/federation/page.mdx index b098fd5..d832007 100644 --- a/app/federation/page.mdx +++ b/app/federation/page.mdx @@ -16,4 +16,5 @@ Federation is built on the [HyperText Transfer Protocol (HTTP)](https://tools.ie + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index a9d00c9..850acbc 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -260,6 +260,7 @@ export const navigation: NavGroup[] = [ { title: "HTTP", href: "/federation/http" }, { title: "Validation", href: "/federation/validation" }, { title: "Discovery", href: "/federation/discovery" }, + { title: "Delegation", href: "/federation/delegation" }, ], }, { From 10b6ca1fe928c865c1c84ae39ff23dd24686542c Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 13:10:58 +0200 Subject: [PATCH 100/110] feat: :sparkles: Add Reactions extension --- app/extensions/reactions/page.mdx | 95 +++++++++++++++++++++++++++++++ components/Navigation.tsx | 2 + 2 files changed, 97 insertions(+) create mode 100644 app/extensions/reactions/page.mdx diff --git a/app/extensions/reactions/page.mdx b/app/extensions/reactions/page.mdx new file mode 100644 index 0000000..33f7168 --- /dev/null +++ b/app/extensions/reactions/page.mdx @@ -0,0 +1,95 @@ +export const metadata = { + title: "Reactions Extension", + description: "The Reactions Extension allows users to react to posts with emojis", +} + +# Reactions Extension + +The Reactions Extension allows users to express their reactions ("react") to posts with emojis. {{ className: 'lead' }} + +## Federation + +User reactions are (like every other entity) federated to all followers, and can be displayed to clients depending on the privacy settings of the associated [Note](/entities/note). + +## Entity Definition + + + + + + Must be `pub.versia:reactions/Reaction`. + + + URI of the [User](/entities/user) that is reacting. + + + URI of the [Note](/entities/note) attached to the reaction. + + + Emoji content of reaction. May also be arbitrary text, or [Custom Emoji](/extensions/custom-emojis) if supported. + + Clients are encouraged to disfavour text in favour of emoji where possible. + + + + + + + ```jsonc {{ title: "Example Entity" }} + { + "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "type": "Extension", // [!code focus:2] + "extension_type": "pub.versia:reactions/Reaction", + "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "created_at": "2021-01-01T00:00:00.000Z", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", // [!code focus:3] + "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19", + "content": "😀", + } + ``` + + + + +## Extensions to Note + +The Reactions Extension extends the [Note](/entities/note) entity with the following fields: + + + + + + URI to a [Collection](/entities/collection) of the [Reactions](#entity-definition) attached to the note. + + + + + + + ```jsonc {{ title: "Example Note" }} + { + "id": "01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "type": "Note", // [!code focus] + "uri": "https://versia.social/notes/01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "created_at": "2024-06-19T01:07:44.139Z", + "author": "https://versia.social/users/018eb863-753f-76ff-83d6-fd590de7740a", + "category": "microblog", + "content": { + "text/plain": { + "content": "Bababooey." + } + }, + "extensions": { // [!code focus:5] + "pub.versia:reactions": { + "reactions": "https://versia.social/notes/01902e09-0f8b-72de-8ee3-9afc0cf5eae1/reactions" + } + }, + "group": "public", + "is_sensitive": false, + "mentions": [], + } + + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 850acbc..fc9e49f 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -290,6 +290,8 @@ export const navigation: NavGroup[] = [ links: [ { title: "Custom Emojis", href: "/extensions/custom-emojis" }, { title: "Likes", href: "/extensions/likes" }, + { title: "Polls", href: "/extensions/polls" }, + { title: "Reactions", href: "/extensions/reactions" }, { title: "Share", href: "/extensions/share" }, { title: "Vanity", href: "/extensions/vanity" }, { title: "WebSockets", href: "/extensions/websockets" }, From 228ef3c98dffeb8e4063f9e7466f0cb60aba0409 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 13:34:30 +0200 Subject: [PATCH 101/110] feat: :sparkles: Add Polls --- app/extensions/polls/page.mdx | 126 ++++++++++++++++++++++++++++++ app/extensions/reactions/page.mdx | 4 +- 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 app/extensions/polls/page.mdx diff --git a/app/extensions/polls/page.mdx b/app/extensions/polls/page.mdx new file mode 100644 index 0000000..633e8e8 --- /dev/null +++ b/app/extensions/polls/page.mdx @@ -0,0 +1,126 @@ +export const metadata = { + title: "Polls Extension", + description: "The Polls Extension allows users to create and vote on polls", +} + +# Polls Extension + +Polls (also known as surveys) are a useful tool for gathering feedback from followers and friends. The Polls Extension allows users to create and vote on polls. {{ className: 'lead' }} + +## Privacy + +Individual user votes on polls should **not** be visible in clients. Instead, clients should display the total number of votes for each option, and the total number of votes cast. + +This is reflected in the presence of total votes as numbers and not as an array of URIs in polls. + +## Extensions to Note + +Note that there is no `question` field: the question should be included in the `content` of the Note itself. + + + + + + Array of options for the poll. Each option is a [ContentFormat](/entities/content-format) that can contain the same properties as a Note's `content` (e.g. [Custom Emojis](/extensions/custom-emojis) or HTML hyperlinks). + + + Array of the number of votes for each option. The length of this array should match the length of the `options` array. + + + Whether the poll allows multiple votes to be cast for different options. + + + ISO 8601 timestamp of when the poll ends and no more votes can be cast. If not present, the poll does not expire. + + + + + + + ```jsonc {{ title: "Example Note" }} + { + "id": "01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "type": "Note", // [!code focus] + "uri": "https://versia.social/notes/01902e09-0f8b-72de-8ee3-9afc0cf5eae1", + "created_at": "2024-06-19T01:07:44.139Z", + "author": "https://versia.social/users/018eb863-753f-76ff-83d6-fd590de7740a", + "category": "microblog", + "content": { + "text/plain": { + "content": "What is your favourite color?" + } + }, + "extensions": { // [!code focus:28] + "pub.versia:polls": { + "options": [ + { + "text/plain": { + "content": "Red" + } + }, + { + "text/plain": { + "content": "Blue" + } + }, + { + "text/plain": { + "content": "Green" + } + } + ], + "votes": [ + 9, + 5, + 0 + ], + "multiple_choice": false, + "expires_at": "2021-01-04T00:00:00.000Z" + } + }, + "group": "public", + "is_sensitive": false, + "mentions": [], + } + + ``` + + + + +## Vote Entity Definition + +If a vote is cast to a poll that is closed, the vote should be rejected with a `422 Unprocessable Entity` error. + + + + + + URI to the user who cast the vote. + + + URI to the poll that the user voted on. Must link to a [Note](/entities/note) with a valid poll. + + + Index of the option that the user voted for. This should be a valid index into the `options` array of the poll. + + + + + + + ```jsonc {{ title: "Example Vote" }} + { + "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "type": "Extension", // [!code focus:2] + "extension_type": "pub.versia:polls/Vote", + "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", + "created_at": "2021-01-01T00:00:00.000Z", + "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", // [!code focus:3] + "poll": "https://example.com/notes/f08a124e-fe90-439e-8be4-15a428a72a19", + "option": 1 + } + ``` + + + \ No newline at end of file diff --git a/app/extensions/reactions/page.mdx b/app/extensions/reactions/page.mdx index 33f7168..0b150e4 100644 --- a/app/extensions/reactions/page.mdx +++ b/app/extensions/reactions/page.mdx @@ -19,10 +19,10 @@ User reactions are (like every other entity) federated to all followers, and can Must be `pub.versia:reactions/Reaction`. - + URI of the [User](/entities/user) that is reacting. - + URI of the [Note](/entities/note) attached to the reaction. From 596b42474f409fa07c3fa9da27260468ba1b5eb5 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 14:29:54 +0200 Subject: [PATCH 102/110] refactor: :recycle: Remove extension_type in favour of using the type field on entities --- app/changelog/page.mdx | 4 +++- app/entities/delete/page.mdx | 2 +- app/entities/page.mdx | 5 ++--- app/entities/user/page.mdx | 2 +- app/extensions/likes/page.mdx | 10 ++++------ app/extensions/page.mdx | 6 +----- app/extensions/polls/page.mdx | 6 ++++-- app/extensions/reactions/page.mdx | 5 ++--- app/extensions/share/page.mdx | 5 ++--- 9 files changed, 20 insertions(+), 25 deletions(-) diff --git a/app/changelog/page.mdx b/app/changelog/page.mdx index 94a207b..1f9874f 100644 --- a/app/changelog/page.mdx +++ b/app/changelog/page.mdx @@ -36,4 +36,6 @@ This page lists changes since Working Draft 03. {{ className: 'lead' }} - Add `algorithm` to [Users](/entities/user) and [InstanceMetadata](/entities/instance-metadata)'s public keys for future use (only `ed25519` is allowed for now). - Renamed the second `public_key` to `key`. - Renamed `Announce` to [Share](/extensions/share). -- Renamed `prev` to `previous` in [Collections](/structures/collection). \ No newline at end of file +- Renamed `prev` to `previous` in [Collections](/structures/collection). +- Removed `extension_type` from entities, and instead use the `type` field. +- Removed `VoteResult` from the [Polls Extension](/extensions/polls). \ No newline at end of file diff --git a/app/entities/delete/page.mdx b/app/entities/delete/page.mdx index 5495431..3ab71ab 100644 --- a/app/entities/delete/page.mdx +++ b/app/entities/delete/page.mdx @@ -27,7 +27,7 @@ Having the authorization is defined as: URI of the `User` who is deleting the entity. [Can be set to `null` to represent the instance](/entities/instance-metadata#the-null-author). - Type of the entity being deleted. In case of extensions, use the entity's `extension_type`. + Type of the entity being deleted. URI of the entity being deleted. diff --git a/app/entities/page.mdx b/app/entities/page.mdx index 0e99332..fb036a2 100644 --- a/app/entities/page.mdx +++ b/app/entities/page.mdx @@ -22,7 +22,7 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. Unique identifier for the entity. Must be unique within the instance. Can be any string. Max of 512 UTF-8 characters. - Type of the entity. Only types defined in the Versia protocol are allowed. Use an [Extension](/extensions) if you want to define custom types. + Type of the entity. Custom types must follow [Extension Naming](/extensions#naming). Date and time when the entity was created. Must be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted string. @@ -59,8 +59,7 @@ Any field in an entity not marked as `required` may be omitted or set to `null`. ```jsonc {{ 'title': 'With Extensions' }} { "id": "f0aacf0b-df7a-4ee5-a2ba-6c4acafd8642", - "type": "Extension", - "extension_type": "org.space:Zlorbs/Zlorb", + "type": "org.space:Zlorbs/Zlorb", "created_at": "2023-04-13T08:00:00Z", "uri": "https://space.org/zlorbs/f0aacf0b-df7a-4ee5-a2ba-6c4acafd8642", "extensions": { // [!code focus:100] diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index 22afa3a..64cd43f 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -109,7 +109,7 @@ Instance **must** be the host of the instance the user is on (hostname with opti followers: URI; following: URI; featured: URI; - // Same format as extension_type on Extensions + // Same format as type on Extensions [key: ExtensionsKey]: URI; } ``` diff --git a/app/extensions/likes/page.mdx b/app/extensions/likes/page.mdx index f48f97b..ee49ea4 100644 --- a/app/extensions/likes/page.mdx +++ b/app/extensions/likes/page.mdx @@ -19,7 +19,7 @@ Likes are a way for users to show appreciation for a note, like Twitter's "heart - + Must be `pub.versia:likes/Like`. @@ -36,8 +36,7 @@ Likes are a way for users to show appreciation for a note, like Twitter's "heart ```jsonc {{ 'title': 'Example Like' }} { "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "type": "Extension", - "extension_type": "pub.versia:likes/Like", + "type": "pub.versia:likes/Like", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", "uri": "https://example.com/likes/3e7e4750-afd4-4d99-a256-02f0710a0520", @@ -57,7 +56,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis - + Must be `pub.versia:likes/Dislike`. @@ -74,8 +73,7 @@ Dislikes are a way for users to show disapproval for a note, like YouTube's "dis ```jsonc {{ 'title': 'Example Dislike' }} { "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "type": "Extension", - "extension_type": "pub.versia:likes/Dislike", + "type": "pub.versia:likes/Dislike", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", "uri": "https://example.com/dislikes/3e7e4750-afd4-4d99-a256-02f0710a0520", diff --git a/app/extensions/page.mdx b/app/extensions/page.mdx index 05cc9d4..54b6b6e 100644 --- a/app/extensions/page.mdx +++ b/app/extensions/page.mdx @@ -87,9 +87,6 @@ Extensions can be found in two places: an [Entity](/entities#entity-definition)' - The entity type. **Must** be `Extension`. The extension type is defined in the `extension_type` property. - - The extension type. [Must follow naming conventions](#naming). @@ -103,8 +100,7 @@ Extensions can be found in two places: an [Entity](/entities#entity-definition)' ```jsonc {{ title: "Example Custom Entity" }} { - "type": "Extension", - "extension_type": "com.example:poll/Poll", + "type": "com.example:poll/Poll", "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", diff --git a/app/extensions/polls/page.mdx b/app/extensions/polls/page.mdx index 633e8e8..c3e70d0 100644 --- a/app/extensions/polls/page.mdx +++ b/app/extensions/polls/page.mdx @@ -95,6 +95,9 @@ If a vote is cast to a poll that is closed, the vote should be rejected with a ` + + Must be `pub.versia:polls/Vote`. + URI to the user who cast the vote. @@ -112,8 +115,7 @@ If a vote is cast to a poll that is closed, the vote should be rejected with a ` ```jsonc {{ title: "Example Vote" }} { "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "type": "Extension", // [!code focus:2] - "extension_type": "pub.versia:polls/Vote", + "type": "pub.versia:polls/Vote", // [!code focus] "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", // [!code focus:3] diff --git a/app/extensions/reactions/page.mdx b/app/extensions/reactions/page.mdx index 0b150e4..14dda9b 100644 --- a/app/extensions/reactions/page.mdx +++ b/app/extensions/reactions/page.mdx @@ -16,7 +16,7 @@ User reactions are (like every other entity) federated to all followers, and can - + Must be `pub.versia:reactions/Reaction`. @@ -38,8 +38,7 @@ User reactions are (like every other entity) federated to all followers, and can ```jsonc {{ title: "Example Entity" }} { "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", - "type": "Extension", // [!code focus:2] - "extension_type": "pub.versia:reactions/Reaction", + "type": "pub.versia:reactions/Reaction", // [!code focus] "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", // [!code focus:3] diff --git a/app/extensions/share/page.mdx b/app/extensions/share/page.mdx index 003d7f3..59c8396 100644 --- a/app/extensions/share/page.mdx +++ b/app/extensions/share/page.mdx @@ -18,7 +18,7 @@ When a user shares a note, the note's original author **must** receive the entit - + Must be `pub.versia:share/Share`. @@ -35,8 +35,7 @@ When a user shares a note, the note's original author **must** receive the entit ```jsonc {{ 'title': 'Example Share' }} { "id": "3e7e4750-afd4-4d99-a256-02f0710a0520", - "type": "Extension", - "extension_type": "pub.versia:share/Share", + "type": "pub.versia:share/Share", "created_at": "2021-01-01T00:00:00.000Z", "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", "uri": "https://example.com/shares/3e7e4750-afd4-4d99-a256-02f0710a0520", From 4a7dd41c454c0de2fd7780069920b6a4fd913bf4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 14:34:25 +0200 Subject: [PATCH 103/110] fix: :bug: Fix broken links --- app/extensions/polls/page.mdx | 2 +- app/extensions/reactions/page.mdx | 2 +- components/Resources.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/extensions/polls/page.mdx b/app/extensions/polls/page.mdx index c3e70d0..a32f3ea 100644 --- a/app/extensions/polls/page.mdx +++ b/app/extensions/polls/page.mdx @@ -21,7 +21,7 @@ Note that there is no `question` field: the question should be included in the ` - Array of options for the poll. Each option is a [ContentFormat](/entities/content-format) that can contain the same properties as a Note's `content` (e.g. [Custom Emojis](/extensions/custom-emojis) or HTML hyperlinks). + Array of options for the poll. Each option is a [ContentFormat](/structures/content-format) that can contain the same properties as a Note's `content` (e.g. [Custom Emojis](/extensions/custom-emojis) or HTML hyperlinks). Array of the number of votes for each option. The length of this array should match the length of the `options` array. diff --git a/app/extensions/reactions/page.mdx b/app/extensions/reactions/page.mdx index 14dda9b..58c885f 100644 --- a/app/extensions/reactions/page.mdx +++ b/app/extensions/reactions/page.mdx @@ -58,7 +58,7 @@ The Reactions Extension extends the [Note](/entities/note) entity with the follo - URI to a [Collection](/entities/collection) of the [Reactions](#entity-definition) attached to the note. + URI to a [Collection](/structures/collection) of the [Reactions](#entity-definition) attached to the note. diff --git a/components/Resources.tsx b/components/Resources.tsx index 1346c10..3cef329 100644 --- a/components/Resources.tsx +++ b/components/Resources.tsx @@ -40,10 +40,10 @@ const resources: ResourceType[] = [ }, }, { - href: "/security", - name: "Security", + href: "/federation", + name: "Federation", description: - "Learn how to secure your Versia implementation and protect your users' data.", + "Learn how to federate data across the Versia federation network.", icon: "tabler:building-bank", pattern: { y: -6, From 141f90092917bd751930bd0b47d1bed56f46362b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 14:38:13 +0200 Subject: [PATCH 104/110] fix: :truck: Point links to new address for Versia Server --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 28183ef..1132da0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -83,7 +83,7 @@ const Page: FC = () => { Versia Server, which uses Versia for federation. This community could include you! Check out our{" "}
    From dcb9c4ca32aeafe830fafc103137dc74465d3f56 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 15:31:04 +0200 Subject: [PATCH 105/110] refactor: :truck: Use Versia logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9efaf86..3f2b023 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

    - Versia Logo + Versia Logo

    From 7189a6f411c0549408fe1092d6b79b2ec09fb298 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 24 Aug 2024 22:05:43 +0200 Subject: [PATCH 106/110] fix: :bug: Make some more User properties optional --- app/entities/user/page.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/entities/user/page.mdx b/app/entities/user/page.mdx index 64cd43f..f7a6ab8 100644 --- a/app/entities/user/page.mdx +++ b/app/entities/user/page.mdx @@ -43,10 +43,10 @@ Instance **must** be the host of the instance the user is on (hostname with opti - + The user's avatar. Must be an image format (`image/*`). - + Short description of the user. Must be text format (`text/*`). @@ -87,11 +87,11 @@ Instance **must** be the host of the instance the user is on (hostname with opti } ``` - - If `true`, the user must approve any new followers manually. If `false`, followers are automatically approved. This does not affect federation, and is meant to be used for clients to display correct UI. + + If `true`, the user must approve any new followers manually. If `false`, followers are automatically approved. This does not affect federation, and is meant to be used for clients to display correct UI. Defaults to `false`. - - User consent to be indexed by search engines. If `false`, the user's profile should not be indexed. + + User consent to be indexed by search engines. If `false`, the user's profile should not be indexed. Defaults to `true`. The user's federation inbox. Refer to the [federation documentation](/federation). From 8a9935fde0e56b0c2d8ad9324f3f2a25e4151505 Mon Sep 17 00:00:00 2001 From: Gaspard Wierzbinski Date: Sun, 25 Aug 2024 15:30:19 +0200 Subject: [PATCH 107/110] fix: :bug: Fix incorrect type definition --- app/extensions/vanity/page.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/extensions/vanity/page.mdx b/app/extensions/vanity/page.mdx index 437b819..3b6d3bb 100644 --- a/app/extensions/vanity/page.mdx +++ b/app/extensions/vanity/page.mdx @@ -41,8 +41,8 @@ All properties are optional. Audio format (e.g. `audio/mpeg`). - - An array of pronouns the user uses. Can be represented as a string or an object. + + An array of internationalized pronouns the user uses. Can be represented as a string or an object. ```typescript /* e.g. "he/him" */ @@ -57,6 +57,8 @@ All properties are optional. } type Pronoun = ShortPronoun | LongPronoun; + /* Example: en-US or fr */ + type LanguageCode = string; ``` @@ -132,4 +134,4 @@ All properties are optional. ``` - \ No newline at end of file + From ac96922bd3df7e97b8d7bebfc1662085ca67eee3 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 25 Aug 2024 18:42:04 +0200 Subject: [PATCH 108/110] feat: :sparkles: Add Reports --- app/extensions/reports/page.mdx | 53 +++++++++++++++++++++++++++++++++ components/Navigation.tsx | 1 + 2 files changed, 54 insertions(+) create mode 100644 app/extensions/reports/page.mdx diff --git a/app/extensions/reports/page.mdx b/app/extensions/reports/page.mdx new file mode 100644 index 0000000..48c4de0 --- /dev/null +++ b/app/extensions/reports/page.mdx @@ -0,0 +1,53 @@ +export const metadata = { + "title": "Reports Extension", + "description": "Reporting can be used to report content to moderators or administrators of a remote instance for review." +} + +# Reports Extension + +Reporting can be used to report content to moderators or administrators of a remote instance for review. {{ className: 'lead' }} + +When an instance receives a report, it *should* be reviewed by a moderator or administrator. + +## Entity Definition + + + + + + Must be `pub.versia:reports/Report`. + + + URI of the reporting [User](/entities/user). Optional if the report is anonymous. + + + URIs of the content being reported. + + + Reason for the report. Should be concise and clear, such as `spam`, `harassment`, `misinformation`, etc. + + + Additional comments about the report. Can be used to provide more context or details. + + + + + + + ```jsonc {{ title: "Example Report" }} + { + "id": "6f3001a1-641b-4763-a9c4-a089852eec84", + "type": "pub.versia:reports/Report", + "author": "https://example.com/users/6f3001a1-641b-4763-a9c4-a089852eec84", + "uri": "https://example.com/reports/f7bbf7fc-88d2-47dd-b241-5d1f770a10f0", + "reported": [ + "https://test.com/publications/46f936a3-9a1e-4b02-8cde-0902a89769fa", + "https://test.com/publications/213d7c56-fb9b-4646-a4d2-7d70aa7d106a" + ], + "reason": "spam", + "comment": "This is spam." + } + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index fc9e49f..fe4d12d 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -292,6 +292,7 @@ export const navigation: NavGroup[] = [ { title: "Likes", href: "/extensions/likes" }, { title: "Polls", href: "/extensions/polls" }, { title: "Reactions", href: "/extensions/reactions" }, + { title: "Reports", href: "/extensions/reports" }, { title: "Share", href: "/extensions/share" }, { title: "Vanity", href: "/extensions/vanity" }, { title: "WebSockets", href: "/extensions/websockets" }, From d96a3531aeb7da2eda05a5800a16bfdae28b78ba Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 25 Aug 2024 19:16:34 +0200 Subject: [PATCH 109/110] feat: :sparkles: Add Migrations extension --- app/extensions/migration/page.mdx | 99 +++++++++++++++++++++++++++++++ components/Navigation.tsx | 1 + 2 files changed, 100 insertions(+) create mode 100644 app/extensions/migration/page.mdx diff --git a/app/extensions/migration/page.mdx b/app/extensions/migration/page.mdx new file mode 100644 index 0000000..32e5e19 --- /dev/null +++ b/app/extensions/migration/page.mdx @@ -0,0 +1,99 @@ +export const metadata = { + "title": "Migration Extension", + "description": "Migration can be used when users want to move their data from one instance to another." +} + +# Migration Extension + +Sometimes, users may want to move their data from one instance to another. This could be due to a change in administration, a desire to be closer to friends, or any other reason. Migration can be done using the migration extension. {{ className: 'lead' }} + +## Behaviour + +Migration happens in three steps: + +### Prepare the New Account + +- The user creates an account on the new instance, and puts the URI of the old account in the `previous` field of the new account. + +### Request Migration + +- The user requests migration from the old instance. The old instance checks that the `previous` field is set, and creates a migration entity. + +- The migration entity is then distributed to every instance that interacts with the old instance, including the new instance. + +- All instances that receive a verified migration entity (i.e. one where the `previous` field is correctly set on the new account) and support migration **must** then move all relationships (followers, followings, etc) from the old account to the new account in *their* internal database. + +### Complete Migration + +- The old instance sets the `new` field of the user to the URI of the new account, and marks it as "disabled" in its internal database. + + +## Entity Definition + + + + + + This entity does not have a URI. + + + Must be `pub.versia:migration/Migration`. + + + URI of the [User](/entities/user) who is migrating. + + + URI of the destination [User](/entities/user) on the new instance. + + + + + + + ```json {{ title: "Example Entity" }} + { + "id": "016f3de2-ad63-4e06-999e-1e6b41c981c5", + "type": "pub.versia:migration/Migration", + "author": "https://example.com/users/44df6e02-ef43-47e0-aff2-47041f3d09ed", + "created_at": "2021-01-01T00:00:00.000Z", + "destination": "https://otherinstance.social/users/73e999a0-53d0-40a3-a5cc-be0408004726", + } + ``` + + + + +## User Extensions + +The following extensions to [User](/entities/user) are used by the migration extension: + + + + + + If this user has migrated from another instance, this property **MUST** be set to the URI of the user on the previous instance. + + + If this user has migrated to another instance, this property **MUST** be set to the URI of the user on the new instance. + + + + + + + ```jsonc {{ 'title': 'Example' }} + { + // ... + "type": "User", + // ... + "extensions": { // [!code focus:100] + "pub.versia:migration": { + "previous": "https://oldinstance.social/users/44df6e02-ef43-47e0-aff2-47041f3d09ed", + // "new": "https://newinstance.social/users/73e999a0-53d0-40a3-a5cc-be0408004726", + } + } + } + ``` + + + \ No newline at end of file diff --git a/components/Navigation.tsx b/components/Navigation.tsx index fe4d12d..b5e7e24 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -290,6 +290,7 @@ export const navigation: NavGroup[] = [ links: [ { title: "Custom Emojis", href: "/extensions/custom-emojis" }, { title: "Likes", href: "/extensions/likes" }, + { title: "Migration", href: "/extensions/migration" }, { title: "Polls", href: "/extensions/polls" }, { title: "Reactions", href: "/extensions/reactions" }, { title: "Reports", href: "/extensions/reports" }, From 2e7d58890277058341749ec8556e82aa2099dd83 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 26 Aug 2024 13:46:31 +0200 Subject: [PATCH 110/110] chore: :bug: Use proper logos --- images/branding/logo-full.webp | Bin 28200 -> 29678 bytes images/branding/logo.webp | Bin 5434 -> 9582 bytes public/favicon.png | Bin 5613 -> 4086 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/images/branding/logo-full.webp b/images/branding/logo-full.webp index 8c01cc4f24ba1a576c54c6ccc0e35163b0d54228..26d297d8a5b3122249a8331b2ea49b1fc344d850 100644 GIT binary patch delta 29286 zcmZU)Wl$VU(5{U;i#x%c;O_1c+}+(_A;{wH65KTecUUC22MrQf++~3f+|KjX`K0Qc zs+qrCQ`PtM++BT5%_c1L7#5_ZBrkvD4FjVuC#|8UA!vXG0|SHo-~NF8pCzTLp(vpT z0|QHraWgKxa>n}3~pFuHZW<6niATukY?>%X1HB366T zCcTZ0n$B6|LF|zdI}I*R5|nUiK@5DPXbU791g?p33+KeE%f2~KfPJz4slBXXDxH#6 zxjd;ShKsDv+)p#gk^}y9Dy8R->iRJCj2Z80BHi)WYec1VdZl!ifVszp@e%mA-!nY5 zwSh;J=`fw1`5z$R(=$bw*j+9f@-EkX#q~aZ5dneSduOb!tnB0L)lL!dP5QT+So*iu zUZ<4^xMOPJT9hHs3yvgN_QbN4(x8fFD8DU1o9>eg=FODp)T#{O2Uzkj1oZOuH%J5+ z@P600+bCnWKWE^gcD$ZLVX&(@$mf$uJU+suGP4NaMq=NffPFuIE(Tn84-AMGcHPHN zM|CVjU}64aJI=KW71*j$1P_tPd8pxiSR&IL$H^HPGj!psnYE4n0})~?KR~nG2YPi5 zGt6LVL95zj70`Ot?QN{`G3gFM-6E9I0qs`RTsy6*)1eJDHSE(@byqD%Se_MXXEs9Y z2hm4$Y%!ml-*0{I&z>Kl2~t(I2w^!5g5RJwLYrs74Nw%8oS$$eFyp?~svho~>lSw= z{0rqnz98nYFS`-J^!Qrv>X!S7RIwBR_jC9yAY?b0PfWU+-k*w{X^cw0E(nHX;DJBd z_7TU1HN!h`l&9Oz{sQ_02w(z++3Aso3XCV3kJC=ep=@?}grr65Z!siaKEV@T+ys~s z>Z*GAmr|bTlCh#9{DqkcI(MUikH6lZx;~Nt#@#xYpkIc3G;CkVA|AG){(3Jm7)R5^ z0hu7J9f(sB>Uyb`5_Id169H4Gzs@gQw!nP3e1A_G-^L=9zWAM<;TUp4+r}kU5^up} z&TB`FuF&SGDBvtHO#bL#F*(`AyL4;n=#D6ht51v0QZAVJ5Kkes5BuYb9d2I{y7fni+vK zqO3v$4HX=4$3eX}ck?afo$hB@4Q#9tFLk)mE%AP@Cu!6tYg-!Nuel*T5;^M%-SQG* zpfW94GgxA2pD+R2Rlz~!Mef*aG*Sd^5F1vlUWTdKmegO=pE1e1m1ScM?KJmO7f5Ff zLWwmX6SGSIkPtX(9*)<5g6h~SPF7|)=WA@nM0FpQJk#CU2Mx$vEr0{tMW}`jpVNl| zbw<3Y*87)ZePA*(U;^@-+SNFVtX{L6#7EJ z0ow0a1=i>e5{!~29 zFs4T-Z!YOMw1pmQCI z<>p6CG9uLW1EeP8{EbDCfpp{Y+&y_nM;azY*InG`o!j~5(3sT%06xIxv(lt&M#)aO9eY&I!}0RVI#4W7 zl#`HjtX#jn0qTQ(MT-%e-6s&kFB>d9P|wy^;WNsHMHv;+I9E-_nQhhA+b+YDuf#;J z-|w&sYWaT2Ao)Y8%1~P;6x;U!8|ZafC!|pvg9iqrO>O$Djgh`Wxt!P`gFigkWgG zUb?>;`PxRDzZ&HEA;bh|0}TCPkoIe@OE|$q3#w}3AI42@g`0@uwwY-`N3}ssUnm766b1a%+~sAT0{))WH@XMUw0s~6(n`z@ zF(*oUjs`iRrT2b8_?hL6!5h_qK^(rhGNiUZYF)l)(!M%Ad?jA3PEq&=m&C2IB`kTp zlF4u6pzZoRHo~hw!>fnxE0v#O{kkRYI^XX86~Gc7zp|rfPJI^ehY`6W^JJK9CrQd4 z^Zsyl1@4)tdAE#}?DDJ6+!?xuz#`mU2x|%5f)t2d*A?Ltxfrq?$EFSb{ahh(s0M!JEw`)!1bu<|vR|+7beIRKn{uIM* zY-4z5?wZcY3uLCu>*3>dG*MFuj#oD8c#_9=z zh%z@ZN8U?$N&51Hv5@DPL&MzTX#OU*Wx)3A3-N;Z{bC7~njU^f^Gk6!9!)$}XaX(j z!tTrr9w!xidvyP2)dHKEFU$MviHYFp!5zc%o5hCLU=!L|1-QpQCqggUSfF_khT+>Q zNi!V35a9)gb58FQO31`ex(&lV0sQo(TPZ)IBn3nZw~@^J!^WZ`H)I+6tcih^h?uA8 zBG>1#_M8jl<;sMEn3h^2#;=iwD`xbeQiGl#@LoPg&KnW%WCkiE`53aSo*2R*T^T+< zN-~4L$^{2-`1XSsEjs6>to(z37NmK^emCJvo605WYZ`2L^lAX+aDoP&d*d-B2Kipq zwEMQ&;_-k~@4A=&&h%hw(;LP(Cqx~faUk1^K$feGW#FPEoTyP zEHz!jC>JWaT+yxBA%sXLJ_)o2>0|_KlRc2FL2{J`{Mkqt`PQsw?ut~$VCQ!T))EzM zzB_1WW?)eBk&+hMWD!$r6)sAZfAN=3O+Eh2Lfoc|Eg+tvW#~k5iHx9XIOaG-8wnp_ zcNR8_KNtaR_>s=r#g$jA{s=#z+6&7$|EHZBZn=th3;F(ZELkD`@uAcQBxxu4UyZp$ zJ^>u13TW7MQTa#YGc|V`D2DL5Qa3R##`{^?BK5w;00Hmo_KZ*tiwFL-yd|Ya)YT*{yIMxr)GfvPMK|#+D#NFsHk^b+ng|KM?L;RDh zl}UNxNKdd$f5Su?yU^8N9Wn3LP%0dos0X4UooW+Yc{#>-qhYdp%U86KExQpbN?Z!G zk50=KNe&(Yp^oI3Y@6=Glpx^w1_SU!mL|3AC$1^asp(i3c8O*+oVC!*Pjb)+UCZXC zSC^qUNN4hRJNVgYbGleNk^G#HhKPtI6m@Bl33#@LM~4*Aa4Aahx3kkFuEmIm@O{sxJ`{vEln@e z3rFwN-CvH_b@681g%-ZZ6%Mv=U6Q>2ayN@Ii>F5k>`_EDV~UZfE4F7}-E)x_9RSdP zFZEuChhM(MZKh>CKMFLxT(?iOJi(*Mof#-1^8V=iRezdE{996pTr`O$9bV&)W6ORL zXv4PoE^2s7mc?+r-lXdTKLAPpcPJ5#I{I(Kg@8uu($Hu4+MG1d46_2J& zYS`TX4qo1g1KKfiNkJuXadsu47dnDJNW5_nb>Jn6T;>TYR+a8Dn=qY%$l>2Q#}+uy zoku=_lQ<)|g~CeOxKvOU+P(w%O{qo*od)Sgh{z^f(UtI1ON$14Ai0TzVQ4n?|E9~k zw;z>vumW(Rd&ESq4GX?qG*@{%KlU6wjAMzVB;fZhj;y4vH5PPyLn>mrUGrN8N#^*j zyh78tQmb1vw^1`Eyx6h0*lzw5_>#fR(R4_G%KeguKofY-~I_)Q{Yhr;2QH^RnVZCAW>CA#X ziz=fuV8_rr)zn=(GwFf^cy0@jMQDZ;$J~F0DuRYAa&K_?!aOITfG)?@-4Q_70%)R8 zUsKoBKyb`x*2aIBM%)UH?uNCoM=6yo>kX}$q4pKF?Z(j~rCy*h9i1Cuh$QCU{PxR^ zS8C!BN+DovQj-63mo_zi6Tj3x2&bnYrfsbV~X2; zgUxbEY>&sMPO(I5MivkNo8Nf8L!Ed&3umOd6GfuZ0=H6vgG}}R6KBQ=x#fuXl~s5d ztQDue>iF`~&-x-#gGYE#SXPRh@K}GH0`x0X|D*T+bPPN=(g5jA4dcXp6=h1F;lS5#OAd~ojA_i1#PH>H(-h}!MVC-*CvM9rZ=g_do7@m?d-nrn& zGp3WeJ6w6h^8DwuHMegh7NKLzrJ^ST^fkviEWg57>Oz8MB##ME{V9~N>2ON2ZphY5 zH(Aw6hCU5izh|K1kb)2|00s=(N1Ro~N`}StZKKvzjLR%?q(FdJVJc$u1-71G4@{+` zYPRR_75f~ayxd|L9}}x#5b3(ZGtzqkA(H+3m-kkI{K4=TswU|ZhNC30Y>0^?>_Fhi zDwg)ZBi}idB7WOJ%V(w{VBd{VQtp8Zk}E@lE}>@`{xaf!8hAj)_9xf)eM}c9*h*vZ zB|pB20Z~>X8i(ccjU-o5m1~2eo_s#+Byp1ox<@v^ifOiVFl>WGXwNq-tiytel-VI7 za)EjYa7KuG{b}84o(^Lh0zp4JPohnOrP+mnW9sMp++Ds>-1 zfHe%HCdTZ2XBE8L(rRv&bKb}xabvNH(Bh5HPp@Dy+qF?)i7&h~F!PwCG^xbi$vRAy zl8A#%MF*bU0n2<31ZI+eev#cwHJM?*8J&?p42lRF(Aw|gvK%^E1sqywlL)?l#9`8s zL2w}t@62(Y+%<($&w2q`sh8kkr#)IZ1DZ}Sa!#4e*~J%d$SA4)>* z)ZX=&AP(Gu0Xxyn!*hR>vuM~3QL>@$JJ74ED0TnoX~D*vfLQCA>-}jVWwfEvbo6ef z2$nEZ#Gy)OOSz$|B1H>9X);lHLYCpuOD|v)uOhsfKW(C|oS)!p&FIJ_s@`Af60fKr z(JB;j^Pf;cGWr_5GKgFaWmdTCR_)hzc0c4qP!>WB!)EZCNeQBuEc39)n9XN{Fp=)> zNjgrs`BFBj%6RqO| zL6`fv=bIttw16Y{j#pnd-l~8e+I}#0mY0fligKspLiK1miI4Aqf19M1fG_ZX?a_@V3EY#5p;vNe4 zd?^2gF`1{~Oh(H35mJsbzM+1q+jWm|;vFKB_?c6twb+#_suQNiG;XX8@b72WSnoGZ`w)+uCG>9nTnJ8Gh9M3Q9oGbpVWCmClg*QPl9FF z0V!42WQ)@G_vX8u*_GlRMIWlWw#bY%0(#AVP=Nveuu$*%lgRr`MP|UoWloy~BudPs zBz1Vu0>3(SgKp!9)B8Uctro3(9MoxJvQ&oSDETmCFgFf+iU*2jLT+G$XIXnHTpY*g z@=4*y_(DjA2afw<{N<{nnFMejqx|yF6nM~=5llz~5)pd!z%oX`4xq9X)7b|LIA7v5 zD==|v-Aq_h%WXB6eD#aNX^7CypGqH3UR~J;e75$hrkI1WoBG6^o>h|i zTr*3f?5VcC_U-sz(DAwFAUJEbL;ZsPLa;asU9dyihmi$Ek?m!jPs)0BxaV_#6yt9k zsdbegto0g^1{a1~n2-l@mQr#>zkHH!k2Xf+ut!@s`qp2On8 zmW@B(ViRtxQbiLjxpC7Pc=6;MMM;r*a>tx6Sz0O|q~ZF584*e(rT$Y<$hX=w`Lmlj zX1dfER0h{kTAHQfCEmIpO1f$V-52R2fo3^~vD@l^zgTT5i@=gF(DxNt=42* z5Yd7Di_p9q$$kefejWWLk0zLwqiV#zPAS1{@HbrF3w<4fCG%PLf?^3rtLKN1vsL}Z zJVpGU8jpAd>z_1w-Q(H<<#40l>rsIdEmwn(2R%c}g1j-5EWyGk?sBd6H@dA44@%>8 z?sZ`zWTKqUqd*hH zlm|>=o#u0vtJGO%dO{ku30*hPC6V&sU?)4OMsWZ*ZMcj^85~Ff`=$rph_o)E2wqD ziiDZrLCr1T;qPjUI>}~Af2piDl1CbpUdg;tF6++kx(O6*q2KIPRVR#y644Tqeh)B=aWd%NY zNk774wTpboiZwjz2>aWbOhe;RiHBCU| z5~rV`d}xvKFvVPxD_R#*Z^>cM|K=2vzuKwpr*wB?1vhy{Y&&As5b z8%{p8zF$oBg44M4d2BHWM_8{|`=7MhJ@UB&{9Zo7uJ%1Rs z7ZMLqaZ}N?)roRW-k)aQK6Ch5x22~xAyf9kP7d^9Z9;Kat~K%qnc3&X;FN4Xnb0uL zAdoyo}G+ zxq1bA^z)1I<|`~g{FP#$vFtX^F89`ytf0Kz?h^MRg~%Hi=-^E5z9Fvd&v*m#G&@y)Mi{0MKMWQ+>ItOQKh z`^@;o?Z!^F-J%4vsv%3**Roz_U^pXsITeH*se#n_vRf>A*fjI>ckC}zMtIiZUdKY! zBLD9RdCReh$CuwpXf#17-)w&|TjFEwip)Zz*T^ig`LX8zKrbWao8{Vufjzsu9|yce z&09RbyOVI#RYqWh@ed>r3?U>YNZB&WvX-xhkQnDA4KD)FFmGj`&$k6zX5|)ZWU|Y% zTFlTz`YF9#?2w1Z*=*Jmal{Tk^F8@acOl%SRR=j1BXAI%LnaOPE^_{5A$$p0pRlN7 zcCcsbi;S4+5Hkj+lSe6Zz?FgcNT-X5UCnOe@+nRpDcx&I(p@$z$*R>co8VI~H1`6< zCgS3j5J&{7{<69^Od95J7E3jg@mmKX2ZEVuzpFE7M#;Zfe z*FGX$QrXd%q(ztJqo3(KHa!Wy-(6d{+tLBH|qx*eJH>1j&iv@7e%#u* zVhDX#hC>L!jV=N$G#HX~mU)uZ?Sot|P(j&qWeEp>kuCm~21bjx-ginhrpmim!rPSM zO-B)!Kr~qGBj3ZY*&sJ8pONV_X*%WO76vuhfBP{Gr}R^X9>?XF5}ok@#dD7EN299G zn1K13X{z*>YtLWJ5?@4PNU-QEitO2De7=7P^p02{hx_J2NcfIohU(!y=N8H|f-eLL z0JaubCAu2pk5+oxeoJ@C_*!;g`kH-i9lFLRPI<~`|BbSqrKZwAEg)0Vs8p9M*p0w{Atxz_VZ8=-5b zQtLvu?uWkcgct|=(UxzmKe#>+u*)`8)XG<=5+sp=y!*FnlQ)^+jZZy>@0a3O*?hgE zSeMnF6g0dD52=5@y}1^gl$o_Q1&8~CG;XnBAJH|MGb)vf{BKNfhv>Wi^L!NuLKe9# z1x>=##QAKQbfdfKBLJ(Hs(Qxr@m~*zg_fvJJRclW(7_d|ct!sGK9AU?&1_TiMiF>e zP4iZvwN$qiKI>IAd!R1T9gAnz`VxVDJmtsC_3`?*H`v&kXIK|wyR1+ z7eH)qf-F2~;?6u?%5LgwB_9O(b1T~U^RrO~U(D(ZsIrT-%8}*|wKU!`8-x=3yEuM& zk$xkzgN?N1qygTxX6ROYvKFQM3$i9Ioq!S)k-`(MQ9*};++ew|hk)yznD8U`j@X=m zg9M3VU{{EE^+nH8-WC-H6MJV1VG;)2>T_oU7z`W5#H(O_dVE*ylpG3LNlRw35=+ej z?9zTz5VJkv{T)#yMeZ5qvF2Dir;aT9^9|Zepn55)S&8v8E()-aUc$~r;uS&IPl9xO z)*>~=kj&GiT>v&G%5a!LHCTS*#vI{x-*b==tPaagzPPIRj;VPxZ|+rEjq|IkeejQb z-R-94Hx2#AN;=~ve_$iXFXx+QHDU#WPOotOIKM@b)^7I|A_AwKnv85FBEUJOhp1K5 zpaMp2O&8+*$bm=tk&43=FRM8Q|6iVgBbcB|FoIMj;o&G|b_T##LZf%`J|L)CMR*&M z$}r_>x{9!qSYvDjAIGH7ZlOc_dhd?Ny0L*gH%Kk82%$~r?*g$!1?U4Y4F`)tqE=eV zQ>RRm&P_AzhUHtoWZ30W>2Tdh0+Fdgxx&OM205+tfrgVe(!Sa@D#(HYG|HmSO*t0U zgU*$#oXL%_C{MGT+tPKQJtsc;=m2_#1Upgu704~G}vk?29ff^CBNO#%+tGtev?Fa z!*tXzklXT>*6u~14;Q#)vl)d~ijOjIrC;!y{U-&W9X;|GoUoJ*6{_Rb4kbR?w2%FH z8Orb{751;Ph~Eh5-!8K<`jK8wOkj1SKd`$c{nF?W6*S~w+t7FmkF$OobUbWKC4SIl zPCn1!xMa001dV#i0%y4Rg!$mgQmaW|nJ7+A_H?@j!K=Vf(toh#hJI&?pC(MC10DyD zLd8{4@M}Ru6IMo_c%_kl`}k+7%K~7LJO?$iah!sCn}RJEX8JvHUg9ObPWlX}rFyFU z@`YEq0DX(2L1m^~R+j3&)nMYJCsh5YvUqJ?pcFb`dPt$yUxeJMa)0Aw811kok=L=v z^^jCPVxPO(*^2?KbEI41z(_^921A==nqI=|&Uq3LPMLNJ=8l=|hGgEhgRM^*nxj9b zSz3D=-ClalQA$I2-0_F)8bl7xwI-bsq%8X1KtR+MsyoAezxKI=Nnr>wMjDh^y+&BF zRgYd$L2dUI+ts>x*Pjqs80dU-5T^2CSu^tk*CoEL9uj;Dmw#3CF8b1CKzdy<(!x$3q53y{jz$Wws+D8Mi{UQu%$>YX*;UZdDS;Fl6?@Hp^HfqiPNCzZ6Tqj&aH{QJL^w-NQZVwFR!`MKI zHQ3|YOgdg>R;qfcg9nojbEe2aYc2ll&R7oV1~azEsT6-Cg4;yA83#RNzgl4u+kxj? ztg8Zj8il@VQ!EkWDcIM(3umPJi4et+R=nPZdC$KfBr^+mLB=8ZXY3$_i-Q@1a}UI# zUm_8tld5>>GWojLtIs|M-K=<_4-v@)rY$emN}2Lzlj$?g${Ioq8x@~HN-tauMw?B4 zRi$B>pU4J~sk%MvvB@#W@FO7Agav0sgJl*JiH@*&GDTer#Jf4HcB=!+N}w^y$3_vnprL*k%IF=OdFC!-AFNLbNCfeC6P4> zyLM`=qgO~;^BadC6Z^@6VeXRGE|g%;1xRv2)fUzI9u|>0c2Hf$hQrQTm;NzH8K6*O zB#BL@wuMOx*}ymk+nc~m`ijNM38701EC5>0St4d-e>~z*?zV&U90aM{D0N5t9~Kbp z!HieOj-{+q)Ohe)*?75EH$V7iX->EMoBJ_D-rSu$0g!O_NEZ^p_{C=YUtOC|^VWt&&t!`DPhEgX`HflXvG6 zMu^;7yHKGmsizuiVla6F*IW1EVqC1C0T5$RR#>3={?i1Y z^$zl2k9B^jH^h>a_yftMS051#>L`p)2>p)4iI$`l^ONU5%NvJ8DY}eNwBGW94Xigh z#3~(*=_|lN9;J#3ulwb{cYLF`orSSpW#|Qgw*u7tseQ+obx*ajUEUz-5&l?DFc60m zdC`ja9IE%@M5^7vS)6L501z?FP4Y2~DzAP2C;SA?F^6x1;)OPE8%r6B>+8SGd3gQO zwk5@fPj;g&yhKtH*qDGuQS>E!6`L&Tq(YdWS*C5Uu?lw$UMua!&6sKN`<`^pg`1js z0V!xo@W4y24|;?6V_N;HH^xt4R)UDfzuB&VVWdVYS0ot{U3g-<+KS=N(f;{pCf~4` zJ^pyG+~#)?y31!jkMHzbU%SpW(l)I2AjR4BZnMK5+MtcGoUj?fx55=kU@@dT&>l%` z2-!@0pS~fmwr1)NS60ksORRDwvQ6$HJ1)rTr=G!3dPyv%PR5JO49bQLyOt6 zEAC!nkts285b)RHYnYsWn$9M+NCoBK6SQH|4YD5&0=*~A8uz8xWbE0{76x8vRq4{@B#_WV=#i)zt;?2}anyizo>FdQHRHUV#Ye0H&wC#@;bH>9eV@w>mv)p~#)rk@Df{v_77AVFlJ7zr z$dI;DC-qSqxn#BfO(`i=uMhh80$nWz2>u0oJ$M8D>>XIO1CXs<;qr^f<;ysp3%SdX z3Wml=q_D;Qo_*m!D?gDPq3>n#fV$0|Zw6PxX#GTl4jwvtbzJ_CF6{x=lGY`M0GQ!S z#~-XZYLHHO!Z+sJTKNGc2W}?1;=YnppAF;LmSn&>bG!+K;5z0=S5PtVZAr+uKrU=I zP9hYJ)}@r5u?PGY?9r+54nrNAJ^5+DlUf~k>$5Jx(+jt-c<^&SI3c2Iju&w`n%cPg zVU4>+)Iz*#RRy4*{<$UN=JJr}&#L8KETAO`F86Isq%@*ZHz68x`AL}n4Ek5OM=17b zup%IAK45BMseAZU$xHWHWVF_pG9aRM$w`Pg8gb zFPc?u&_?6hf@B;sA|gcUODa6a1dKWjm|a?}kvGA>AoZ2gf6;U>D+v^SjPyE-5Jtl5USv zLo)C>zQvSc_cd13eu+WV!y3Ee!4H#0BmfzekZ* z#ygsR8i_{2tOU=`{x2b~?Z0L)lUuCiY7I1#23cQn6&I@Adm|I2 z#~fD#=(U#L&HKMGlnOGxRJh72PNJ{-ntY$*iVxa?u3R&aNZxv~!r2>sn<)-sM=Gq= zOX-)Dr3}pa-4Cwlo`*VY^(Pn~oHVDkRM4l!`3xUQvA-JKC?jbNvw5Zc<(I; z$JCoT;TPE^ueZ5_wVk< zZhV*e3eMdwarm5oVe18xG0UcN|F^mtvX@l|@OW`UPJXI72H!-Zd@P6EE0koSUtf|J z)Ee{s>c4>Iv&~kn{40B@tB~>Ty8I}iZE&b{R){N82R z&86kG>tfI@*O?3bv>os#x$hzlO1PJ;FH#JL;JTXg9~0`H?KiK{^!YKd{fHBhp+~=d zW9j!-7~8~nzX!TwDwr4?j01dYUow8Rp=jIIw1VCL`gwAp1Lsdu&{0(ucaxtWO{-U? z3$&$zrGehJm|>ZE_ZtYWKuhsJ=()I`3x(v3HrwTs5-%;zf{sq}|9$(>j{X`j&; zrHv-%##K3nk7t9nBu_4K4F_N`&Sbf^v2gj%^YhRWYd>aXv7L#=7o0vqiZioY-1UwX zT7>Ez^#7j#6)q&N2WcRnC)n73&f_Hi-3T$wHS75yS6!iiV=?`sr;IQO7tpzOBo79& zhU7{DqH;6{Pb+GZqy=11l-kKv|H+auAPFoI<3ZR0&Ng#xSi1Fe+LP9UBXZVkIpgOq zp;sVTi(-47B~m$57By94xV>ckhX3fr}Yes>~QeyKq~1u+Chol zMq`ESs^sjnqCCY{ZIN2`w0ZXnIWNIO-iaGZ+DojszY?{O0_3(9%)rVb4(SW*G1)YT z>K^*4r;Nr2Lb6h2@JReVTf`Xg>6hOFs^jM2SJcm?)LX`7eS!OD`{=gIVGcl|sI2xz z8?!3R_DqX9OPt#i51W~}M51>d;S9Sbu%J{SxW!61BKPKjE83q4&M9j6?i@vA2@)E} zLNAigH2H&1GO8>In|xZ%X2CwX7)C0aw-eegN&vM1)yW+4!G_adFl$MXoHm)A?mEq{ zRF~!|$Ntk5mFM_g6?^>r&dMoMyViB~B{Pl=TB|As;pKWK*W!2V2vQxN(6osheV)GyerZs@+hnr}u0V7bXjUcN$pOy7C2tJ6iH{4K`oXMcTByV{7RJ!ggXHB`b;& zuIQ*DudKbJx)7b}Q3uNo>1=7;VC@f_Gz69_>68)UUlx8j9qxD&xFz6{$9 zg>FK)Nr9Yyk)yLnFpUyqPoK5OnLZT$MZ*>cecyb4D@LntLl5` zpncCqEghy?qFr_Q)soFv=)P(G*OJB1u%X}YJ8`cGwZ!Lc@qrI&PcgRfoboGjlX;+~ zws30+NuY)TCmD|kcJH8C=9-C1>da#A+BJ*@_Ka^f$E7sT=v^)^c00D`IOyS7y{uTh zCvqN!1_{7uQ|l9;8S0ApGiuBI`P`!p%yG#Iyc0dX!`c9Bt&OujAQUK}M`7DWA7Dqc zFRo!LR6n_zoE&hI*G_+AzlA~7DM7g+BYcL*CAz;nF7l-at-C4FNpwxXAhB zJ};}^o`;S+sdylT)V|~4+N|3wR229hvrq}oi2dX^SYi0|5f1Bs$~MiV@oR}^>Y8K{ zyPr?c_g;Faq}hC~P=qy(s^2z0^|*tTn;JecZk+Z&ibm!WWiZGgZYdVo3Pd6(08I&= z+R`V#YHL?9!Z?=HJRH>!PD80+YLZ=XnYk@o86}uI(D^9-Ae>eXZUV0!uRuN_ZMTx8 zhe4soSpK4!#8j6t0x?|Z-P*ccB}0?NpvS%)IaA0awBe3wvv85v$Dc~0vl)t0k7PT7 z!RUptY4g(4CTp9-^sVt92=rV`3=_4>tcUEyZ`h=erpYnn%PlJ5hoy#Ha`Bo-B)5)rZ4+=u=SoVVz z&(%K@d^t_-((Pai8*5I?9BDq^qDnLey}&}_C1$SLu)oJwN|?OqOkPQTSKiF&zBx=A ziVv5Qf}v7uD{rP(q-pxuCm%;!yh^G@=;2S$;CI&!hcH>-!XMvS%PN+NaJOxBb<7F> zQR@8p2+yZ9r4TDn%vWeM82A7;HtrmXel7{4=O4*9v=dU!3&vY6z3^@(Y%Gm zOW=dwhk*VJ4LPhp!D$HfxW+sD-&o9R6RH1~d?kWH?|>O#%?9T<2$`Dnz7Ab8=hCMT z&+xiU{qa-4!k&mwtOPV*#iE>pvH{-35fAq312+hdG)7~A5FVl{d$GHBVxPhnwKTrU zHy{ym5@4bhX7O;kzTRr*`W@Kl{gES@KXIe#7vn+o#lE~Bk10*YDbe$+{@a@q7H2_|PPTRw$GCN+5f9Wi+Eg#$IOGQt%KWefi87JBjxrI06 zQ|4Yuec|c)04I@<66f^RBF5tLWLo1nCA-rDOC`A$8fuIR|C$(@7yLwolv$W;0K(lk z;QJEuG+W=wMh{`)Aj;V9Bk`A@eN7pN8W#wkiujzp{ExM2fi|D$=SA#Nls#-qCr_&f z{7KQuUcg9CXV(Ucz_zJpYf?IT4BHGba=PD)vx%f;QlHW5U=W5ae7;RC6miQf^al*o zUbAb~1z0W`yFAXZ8sJ8|EOX&lAfe0)Hj=Ah_{dW)) ztQ6)U^d*J%+^Tt8rbUAEO~OSJfGeMTb{Ucq?caH{aJaR1IcA+>AV-hTQ>@6L(JU?$ z75H?$v!HoEiI9LbpA#(}rncuz8PqfRhd-e*bTTRf!lbD>`ab5!v|U~ls!hFy$>M5!^gyq=(g%f>v;#2nFVeqwhv+D z(E__(W*oN&DSmd6Ua*^N{KjWtI^2zw?IW@DK?n}U)BW3z=VJ>D7~LTt@+mg7skqdV zb(be_Aoc1IBT8@UB)bYB85_{nMM;UX`fA4Zscr$OXqW=G6;fU7rbYNj;{npHHq5kZ z3tVPQ(o7<&j{IBl@vWpt`@5k2rC4%gpCvvd6MG>VYOO9%UxxJ?CGrV6EjFjlDT!u# z0Xtn%(QV70tmw7)(fnY4MZuG@^O^=Z-ZY-%JG{Ac>=3cYeND?h|88f(s_6qL*ZhI+ z___oVu14^t!Agb%{}~*!{EvS^X5VuF26QylnX6--^)@~Fq>-p?{{$^e?XZAu59IU2 zR=va&1B|eD#m_LSC%(n_tud5CX!}Fjo)6O~5aPm_h_p>6d#PWBJI~#wU=CMrV*FA% zZtI#?Tm?jQGI7rN{_tf!rY}2z1KE&vlTB9X=EijBg zc;-lGo+*%Z$>RLxsL^OR;%LbS^2i^b+9ZkF4?|}yU;@2_`a5SRMY!HWYtL#n?IyQ3 z#2ovY6)N?=MMthr!nZ<_xG${!A372V6Gt&oNC9r@C+G^U&Y-wd?i8x{K*wF)A&m@( z#!Z7pYV%`aZg6SiTygnhq+9BPDCM+s(qtk_86^!vuh(_M6TYNgLIcFz(Ld4Lv+P;b zD3$TWlVUN0RBkrCy4nbdg1lAY)><;TkP@w zTKR16O#Db0dLl1PBwaW>v`CfJq4>ycv9zBak^KINpLL>THV&a^j;Tm_*Yd>E@fRMC znh9|mcy$Ldl80zd{;D`gg8zMAW8=?omY@eU!2!Lr$-b-7MpGe3TKENRrOK1$Bg~en zgoby~-=_ZFAap;|FC$`c?O_MMXdu)A@)g5|&zuROAI2s~5{Vtq_Y9b>eFMb%Ev%qn zZtbCx)POB-EU%~dlupr)8@zW)_I_6 zkv%ZFCUYn6U5!sZVXzU#`FKPJaRuW20DNQtQa(>xY!4=b0p5YX|9@OHyq4ZYRyvqo z_EBT=`I+h3SQX}pBN?!g;`S1Xf88<>!vT^DljUWp8z_kkHnBr)G6gvCLD}H z*6gV5L?a<0)!16sPeAwIlU_C0CeBVL5jCg6B@U)BjTL_NQz0j`T*mtKyh*!7wS%|gN00Fl_kB2x2;Jrv>}M&!ez zbry%yX1Wyg*g4pQVWV^C+aL(_{|ewSAI|!q^~GD13B;MjFkEsC5Zq+hq*VXddhllJ zPhBKBka}9e@BW|-dO=(!$=YUVqTq29%A2#W^gf1bX+IU%WuT zxmI*Sdv2r@2ljSMTCncI9yU1J{!Q73};@>Ej4oy2lr=yEwK19fF2VcO49O388GtiF?xc2qo>GIkMfv zr>&=V+t>^T`$MJud)Phl-WBJoOjZ&E+{$z2VT^3p(ssv-D?uyyog99+hb;t%Jdm;; zs`1^Mwf2eFBa|2Kc;vB=BJWs#P(8cE2-m@>O=Zbr$7}2DJ^eAeQ;%l>?h#o>2_T2~ z(=NKiboEjVI`|YJ1aYQW{$6lAc9NPmOoFwjT6eoX`R)gpPBsX?+TjBno`W-@HFQy3 za2aUKJCEc37f9C9hiudxS2mM*0Ka<<6-a>74Wi)JMdYqM-7eWfwcZ#-ktQ%s1jD8w zu`KyI#kgS~ov>Hne#FIiF-RW9Fd2zRSDJ~UG#(8D-Av=Pb`{yudm#bez7qmaprFsc zdp}FnNF(+5=aYCB9|s0@Z)mbNU6bxZ9|d{&0(RGv6+|O{-llv5yKB^sZ5J624(}Oe zBb$^B6r8u01~GKD7BqFZa~U9GJ+*pP#HFy$zx&v5LYMjhA9%xlBL$pDOeQUx?2jKA zEP25zxrJ;76-xaoc8}=@Dc#kaHL=!hXb(kgR5-<>F<=p^+~cX6QGw#r)`UA`+4tb? zd#kK4PEBKfgi4&wLYYyi+I|x6f4GmWY?*AGiv!oBKqv$P@ZE__A_eM-DHeGM7r_{A z%SLTt+?HNx5HecWe$S8O20y?&D*XJr+b(p7x2a-^pGXKbS=J6CNTwSQ`#Q@gd)Af$ z-;}}Q8gEX+>}6;qzI&}fN`!T21oy=rrC%;YHE2wKYE!3`W}_(1AKiOZ;yYwcFs_w) zwOtEvFSRbDRr+}p5Ax}~lT51C7X3U>i}>=_;tX5BCTfoC6v=Y^x3Ie)f=t@7SW5xC zexB?J^Gw!&%Q=)*Bx|Ap&c33IP zYmg9PEM&tWNv!cSn}u;uvKsWSixdJLKppjjA-Z+}h7#o=k+jbz_D$-6%YK zT0Z~oyp%yYjTV*DbAJd%4E+cDt{RIAw>Lh??3Js+5pPu1$QTb()8PFIcK>lkX~kKV zDA8bB6k1WV!U-6r>Dbko3q@YuvWo#9n&50V#P;&;>l=~i^qXeAU_8sir z{<&g0Aal;Q?|nv+9y?R17l|t$kWQ1?H|RqWTsMHuwPA%p^O{#d>rHy#q|D|=i>m97{KmslRZ=t4ScsJWQv}u#gMWA9dqd%gE2B; zpx_iId{rBFNy?rwt4grolP8X|gk zdF5G%JFJmgi7r(()`@6PW7{mCgEP4~IL#_PS??G2Vt@U^KY97|yF*}~cakEi1p_^Y zLX`}1>evv_fK-&5ElUt6I#vCK3t`H=NJaSWVD?mCOkkVx=p1sMQxS)Z8l}wxBqbG> zaZ)>=WMc3wtjn~Kw1JJC$_m__ZaD8hIgN+v4zk24Zl?zkAOC_8P57 zMQfLj1dUQGCKPR#4C0yrJBS8AH+pq=y4_KiA^7mS4|6J96qhEm&GxnG+$sXupu^{- zo_}t;p;z>EL69s0rLFVHn63@w`ZjhSvy4&f>q;`j&9TqhgSjFjM=5$skyhmM?LXFW z#DgoLzB)$-U-t#<-aw-`)xQq$##$4<8G8mi_^IlMHZ4?(G-fd&JKX&-Bq&%7ER%lz zlO9SZfAjC|kljm)d)$@93v`Pv4&k485#wNpM%>S{k`19UJ>p5S&kL9P_`BP#t;yiQ z_v#jytfQ!t&K0p1*up7@CW16u3|y@sQg|c1Uga5plCNO*Kk!dpG8UujFq9AN;Knu5 z1WxP~1a%-SUIe-;cUy}V;NHGoE{7~UW|WbMe-bi@o+cE&ieSVILP)X!VYnO|#%ej; zlquCI>60-iy8F@{wBC?@JRd)xiF%lQ=PjNH2k5fNzWrBWfZnSqH4m)6Q_3OEKqhAK zd<(lLWgfcag6+ql#yj7sYbqJ0&0{wJValLtj7h+0tgZpxVn^1+$8CD5^I@8he;-E=dxRiIhVxnW=7)^Wzq^0cXb#ck4zV{7?TLP6IW_dD3VHJwX*KaU6D^0?eIs&RB;kHGh0o3bq|C5p??e}>OM z1p~H!CAPAFz%IaDH4>#=#|zpFX$54BLwAUZUV|WM?PZh!64JDxEhNe`HiSU|^yD5_UiQn_h(^O7$MEdVgac^fXOGI|MWz zrz8{@N2ybv!~Ajpz^zDK_nX+grN@jRi|#s5qWY0+agWoHVWN6CG6zvbB>F;}f+%%G zG;R4`|2^iOkT1KKpY`)u;{M?NADYx?4S(zK=f+v7EmFAGJ|h@e+9Znkma&j zGG`obS0k#e=_bJAW?h__AePPGdo->a(71?!Z{7RbYy=gnL0MbGOH_GpJau#%rwSS zhC-*us~u*|>P{FLXaGeQ=SF^(gIWOk&tm* z+bhDm8v-AHo`<|>XSzipfq8Pu4hsB!BQ{NYLE{*}TCu z^Js7E4c4pDmyz2of2Ja6+!%8L-NxJA{T0FC9YrEk!Qv|vy}OwG-}EY^+a}{KoQNDW z-Zl8~0*c^j5}6Bd8BUK*xdWh1omGLaK=(IW0cmhzGtrQ>D1f<|41&esrWS($Q`~$! z&54LCn`~DP{xk1>0X^6@%V4){?)-Q~9MoQ$C$5}*ctjb;e@F>A+`c3m-GH!?5aB@k z{JRT0&W5y{RyWp)Z=izsC`1cPTVh}|}gYwhwu3C9CPPU6T z$s^^nvbpbY#6gLUcs(0CKapGCI@kilg<{m0c3m7S{O)wLDvBhed{8R|`P%U##%7C8 z+uL=R+>K4ne>@cv$VM-I0hjZ56K6Uuz`YD3s%_b2fRxN#!yiwR2c_0~IYh_rtwk~{ z!`{3FS?7ZaNl5wKM*-)|-e(v?{eN*|ys_sXj>ASc&+W4I5O@E)P;jd5XWxCuIn|s3 zdV|1xoBaeq44v|I2)Qh$AbY%ff3?qRY-dvCP)p=nf9=3MI%;~@hd4Hludz7cQin4! z(3r|>CuiD5?qR5mQ=#$sPBMJ#<6>c856fm7aNkh}Weob_jg*O8%D^Kdt+P(!FYxg^ zY!v+dK6V)aSYGKRmS*38dwMYXrD%5?_|40OB_RiUOAUKH^kh*4YyQhJ>hR@^fe;x9 za2kI1f0)*)&eNJkdyygXDE#p7GCO4&n$&_g+!uMMbY5Vv&sZr!upaSC(0yI7Wuy0g zvxPWOhLafWL|;_(WS}DBTVP{!GY90B*CHm?bz$B@_TYY?`B0*nl^P5b(D8s-5Zz^@ z!ZVL~9hm?fwP-3%+*TNg!{^`K@k3j#(@{jwe~s{!w{y&~n**2usV|j1j<-K;aBjK) zlntB2Ab$b6J4v-|^>;EpbC6Kn$>{C3ms^m#~&cg`(PtYQ(pCZq8btpuCGCN zZS>M<)_L)#F9o6&EW&5&@f465Ehd6;DFBB&r(L2<<~u6~czk>Ty5}}5J}>)#TD`Mc ze|HmgSeu4Cy0B995cFt6W)3u{obJ`#=B7ju4eDp#-8>C6@9Bo~s472R>}kVNMMPu? z!XuTOlO7mddy*wjM9sEzepbE~-4(WK-eAx8yh)E7pYVr^EE2rZ`TYvyEja#3y5Z$} zjMC>ylYa8u8`!1+I<*HhNi62F14s|8e z6~vXko=1Co9$6V_JtV{FITjFpCzH%g1(o9sj{eWTdre}ySr#|Uyj=Frt+V(QYqXm8 z8lq#mpy_%bat6vBkv7aQnHHd|!2R?oS!0A_u9#i_QU|w>aKtu#K+L3^m`j$QvgcWi zLzUwdVTQ&3;dif?EioF#=Q-jAJ-WjIIv;_p5ZI6e%$+s(1F4P0+El--s!|+M??)&~@#TnDo}2&0?cc6R4Jj7PkFOI`T@ z`7o|GjRAFla39A^bg6!6Gi=AxyFcIVE|CXyR{bE;XE+e^@fW*jDL)W^5ku?#W!)1hvT-SQ5#;Ty~B5 z2!57fcYnRzo*j}!1?=<4S_L|u1F8FxoFQ0ieKp|A6pvhX_{uL%p6KW8OAlgRG= zbi3XDRM5-@t#Is*-%`vel#->DpnW=*HQCXM8`q`2a|(4lfTaf|&*woz?*91s1^Cfk z;>wd%RUt+OwY&bLhf#JhsXY-HNrqxfn5B%zh)r*DPf)U)iacCFE$3-}mYhT!*eY%r zMBMJLHyYpGomZvGe4^HrskGJz3y8y7Ka;&x7=Mh~{r!o@?K}Mn>R*{2-!|D*-$@5w zC&>AS<~~BAUX-*Z_w>U#9V#z~K8to2{%==t-Ss}b{7+S!xbPVaFPTjdtc@gQk!50g z)AYi7J%{+{uWx^MRo7APd%(u*{&I7dpNFw`>M&;>4d2!@DymfGC7{C=WgQsU5_YH~ z*njSSG5~a!Ss)Z6&z;!)=gnVU6zkwZU2JSLKd*p84NYX5Q-SOyyxRd~oP}UtyoEb6 zRaAm@EG6_J!gas@xK)>@cj?B2C`eZ-s_2R#boC!5n5e__>qdx9IeNyjQ)<8C9cT_N z8MWB-LS=P-|Ih7Kf4wTZ16HZs6Jjz^hkps?vHm)(38&i}0h<__%)acian8XG#DDXD zuYuJ4uWz^3<>_x7U|k}e);`&XRErCWLF`7Mdf~fdwr?zxd8xHP&gof9RL8Zya1g6DgXii0zN4eh(jTv zArc%MNEiZyv$uX5H+*L>mJRiCJkp1nruAvN1L1#*{>l4w{rj(VRs5>%2lr*}5&Hks2h11jkM2jiulxU0ztw-xepP&ee=z=kwg3OIchD#IgVTH2 zv-^Mj&r6+L4$OMW@)z{K*ETxeFZlP}AK`yq(h>Y`c%%XUVcdc8cd7qTf9rp6_yGM% z@v`tY`$zeYU=QN|)V&10uk;7?Z?L~f-$q~<(eeD|`^+p#uiN(hzi-?2{l9P9_Wi$a z+xGpxZ)p|Hd`BC9pf-r4Ze>yG+P1B$YTCAxU+Z@bR~Ij%+;y6vSD@EVtH4CPWhc!o zZ_RHTVB@WrEivUhWODc6H@zeJ{yc|g5@Zsc;n{bHTDM@6#0~=T+y@uP0$K)()q|Cw zyxi>oF~R5A=St8*2M7?+9>m-Brxn%iK6IxzK^auk&T_zib|no2nAldbo(Wtn2he19 zSn2pc$dUr(se6N{X_O_LSdp+-kI_v{Q=we~@}5g}clFI=?0bIw%L=DaMpYetFDVRqBoZkH_oPRVg#3`nJw!OPu<%i`% z4OO}V07@4l)~rB>>~cr6%-EsulGs!9QWwnRRFLTnTe{D#FbCH{VWtg1pVM&I{sL^vLsUa|- zuNv;0LwN`7FD7Dgd-f(%hGSy*b|&Rhvtqs!YmlniKDQt$F|+g^)jmhI{=G*W$mH}K z94KYiu&-%iukQXn>hZB%Uns~5M8Ik%x{M-!NfLV+MwNuM+>=6de=jKB?EBFy-k`Oc zlZ`^!j5-GX4u2^jw40yO|D>VOflx8hxp^lM2DvG9-q;QJ2zTgJ0U>!!$n0*cGd^d01MXk1MZPVFPKdX_uOxP2bz8u3PeMk&hRAM9J{&wGzc3`7 z9k~er|G0i$#mJbtH&eBN!gbUbE_pa#obB+h{R64;OLw29?>_>%9xk=3NNvzLrNjh+ zEHKoxzW19M?@Sq@8l+BD`o>9r^=^M!Oi>2yy*ICik!8vY-ae|js8aXnhYhBWU@&au z5v7frf#UD#JZKGYe{+^!eN~S(^wJ>FkI&9z;Ej{-^xM4Qs!&>}CG_3mRW?pm6JlB@ z!_newr9Csocy$<6<#=BQEqtT@CCFV+YA3j*pOw;F5^WB*k2~Z|oj_WD2?8^Nwjr=U zbY=lJ=m6rg1MOprFGM4VDqCf%(LMNX$2L-KC9e+U;42W$&z3M^*{a!- zA!Q*_2|~rlCK+J;i8lUJHQEkF_l{XXFed_px!H7#;(N2I5JQj1B}nMXMwcCKC%xLV zXlab7l&V8i4IK0JbA$GO;E4tF%GGI_M@gIM(vhT*@iMzhjE>q9hl5U%V~}^?vk5#G zfa?6b(w#Ml6v1-(aH1_`xj^R*?_5n}oJTZgh(i99mFBGb0InR`Ar0{6H?^R3Oq!Ma(ZD3a z#^2kuXwZ|_76o~b)#TGkNL^s=!)keRWvP8ZSps_vdsb%g>>=ZE#4&}Xf2)|aon;{u z#*)hb5mD;@RIP7xAgr22(I?5mV%+UEk{k#74}1g<-fU)n!~Sp?QMyqFqSbWMlPKGLkDq z8ys#zq|1FCNEC>NB%SwxGZWAUnGL*A$b&}J#lE}GLn>YvL5|(eIl;Pg?~7)zGT`Oi zsqi`A|JSj9h8JekBk1@ZuWU6HwMY3Wxm}=&^NaOI>Y2iXiV7x;VKQ7rm4E6)bdbmC z)FX`seJ99Mp3iqm*m2!WdG73#p$k0g)sTK?{vu;!70xAnnsYg?Ik+#Z2c#&k@q)uD z&P={K+gt56AuKl|(=gt@mVjGtrOFZ1#4tx%Z_lJjdFVbA=RHcBvn-tm z2Qeya(~GUIUG$v9zuXG25qA@DiWi;O_mLll^foBGw=Uuj-^!i19O`i80wdSi=;f<~ zmN6cGf7U+YvQ&zQkDD)NAlnVuBJU1iCo}%^;J-?(YJ{#)6DSGwBrzA(D~!@|gE zsJ5jB6L^F9#N52{r3uRcR3(U>5t}P^izWs7Idx6=;%#ph1w4}HQ*u^!Tl+m)fyOR> z3cQ+U==Pz+k&6=$Cr|El&rtlBbzO*(`Pg3lrdbI{Lx=Vksh9L|@? zoB`upBgO-iv|1(;9?moILR!O4WjmRFu`cUE&xYHpUKP||jU8FNY#bDwf;1BQ031@I z{!JmxT8OP+(9c{>thX^(^_KY9da?yicxZ)h}Y;}~D?OQt*f_FVk)vt!_P zCoB0W4@Nhu_%?y*(Boi6X1(!_n^J5zkGWzg@j1O;!L$!sWMyj$mEx0s5LJxU zi$i#?a$g0yLpU+TJ1;G^pwSaFh`3no?MNq>J!r3 zw#gqwoCas$h)l0_bLd8ARD%zW_0JrVWR#lFJBghia|u_omRURJ7)yJI5AWnCgGNtx zmlsD7K?$|Ph~c<2z$@(Kt#+=N_sUdqKPghD+VG3VMOl8Q+-U`?-yPx44o+eR(BMKb z9n81`z&ifZu&DzM7t&;ZRGb7ugoUv#=y0m6*U!IwD$zZ5znorivV`=WDN}cGb8l$M zWYE~pr(P%|REzXSFbJc~o?1w$HeKt6%b!eq!OH9}k>#BUvxiRW(j|g^#b5y_!PMaL zO-w#xlcYqN0vI4JFz#ZmS%t&;d%prbTu)dcP{n;nuhbiKLV_vfd(CQ)2&{Ptd95ur$VAgG+>1@GmGf)*bjCD44!q(VCNZBIz z9Z_MIj{7Gk5&jc0+8F(;EqYfMe%j8*-kv$H^J%D_;VyK4{H{^n9E(@NJdk}KDqy~R zCtn*-4TPD>i~3<3qmTS>3HA-??OLPLjy%petD>-gEjwAdWA1YB$dDq10T4onAhwMG zLOMrGuw%v7l6_I*?xAOLQ1?#$XCtPnt}V++PkTB1L&I2<1RGJRlAOIn-DLdb=vq-! zDlNRlC0F!+HgMjn6slt0?HV)E%bsX(dZ(W(F|L>s_gMs{T!bj&E{b~&UTyid879<8 zY=*RnkdkaK>W$i*WBwFySoS<|3iGh{+k`E;m+0Gb(I;D+JrU*`H*zXKi^QkN%3mYi z`1+R@2$Iu~{8pd*;QLa}FuJh9+X+AsF{RCFH2(R2Ki$d_HcLxfNH+6*W)Zuo{z(9I zb2OnMx~&7xs4)P147uD4m%QZalDfHLx=POpzym1co5 zg#8tL+gJ?0Z?9QFP)j{$ox$;5;o>30(KMF!|5b`%dbQt|tjKvfJ}WLZiy@fVKMg;R z#iM9{4m_7zp?<8CQUw!N_A>tFZ0e1iX&>c2sO`wFX)-MH3$#(lyVo2H*X-@O=#Uv+ zl)`(szx<2@C&l3EIxV^9mfcg8pK{S$3jd>9MU8+xM*Q$2k%QaM0)Fxz8bAPX%l}wS zx}UQ&)doNwMYCCs#$tRCPuIyR)FoY+9@%Yw4`v##j_R7zh$5Wu+*<&4m+8ypYo@V> zX}p7%h?c3S>fnl;5(ei>P0zAe99;z@>=&8!z$h z*eEb!4`XBUABH@&Q!akHu3llq10w=|{GF^|`4WdY_-i?fmB`|`x0!#8`OqUmVO?WV z-)VI?@x}pdvmE8VZ`INdDI(A-I6bqgtte@=E%-kP!3dL>2pmpNldu<=$MQ@Oz55Gc zLuFD7BPD`jDI?oTD`WN2b9TdGOa5Q zR_Yf#OORlI2(Ylv!~E*%!_u^WY3Jf(f9~BA&7O+ho_ml`ravQp$Hx{e$W3{mQ%^T= zQp^Pgz#dli4c3>exZyiQ_7fiRpBP~4!0vWsfx=#)eEw}D&p`lzdvh#*cjZ8r{PI5f zUV2$;khi!X8gR5|7@EiMr743jBN23vGbGzqrELCLu6Ozoq{pQGpW+*JV0`!-7dj@n zBsJ0#bL1?1Zb64RoA4ahAOHY&d0E`Ra*AxP1%BBkMB7sfahY`v*Pw9Tk`&FAOQXMN z<`ogH@NWPkSD!%Uv9x^q&$yzI0r;i)iR(cM3++$=9Ee@(L^p6 zZ8g*-Xh5*r2lV34KO)GcJUj!bq_Jubq1cyS1pbGltA2eZ=V*{2=pd39j_`nHo;5^C z8v9uD>kc4TYMtQ|CtM%9v@PF(h@Pv?M)@{w$X@F5_`_n%_p+^jz@hG#oY>_UH$kBq zazl~jiw48UB${-z=9P4UfC6DhkZeXWwR0sHR=%j+T)y~&T=-kMnRmeLuTzvH-5m3g zcx0ZVXY}ktx58ITWZC8km#|644;S9V{N>Lca<1b=BY~)2aNfl?Ov9+pktfcu3ATuM z^l!3N#3uC&CF#0<_of5PfqAR-#K2egxz^EiFD6h0DYMu@$2bF)IszVN##q}7UviJ$ z(+WegEFy-vz4yEWpRjKL+!5w<{)~9XJ3uAM21JZeX3}#%+OE$KirQVJYUcw~p+#c+ z<9W~^uBqbrKY#<{LQI%mron+gQ*0efw<|(NT+anSq!X=wTk0dbxojzM4SFNtxQ6$* zv5GO^pc+jiV-9H}`Vs^|vp$lyth7(K{|!Jsp^9+qWG276>u3S6{*TQa?;ZwZ*cb|} zcf}x`u$=zbtk&O@Q=8{fL z()QFHN`fPQBmn=R{$Wj!MKF5|vELw{&jFt%P`fh78%6`dSBgS4zuFA#MG5eM`{pi{ z14PaCdwR->U<;JeI%-4II5q@nK&9qhaZG`|gt#~6VlzL-uJmCyRSn3rqK35{$*kcU z8IJo8ZA>KrOBji}F+> zN~Qo5^4xM(jE<1m1KCbk#BM|JTgMse#gldMR=^e6LAYC>9J zu#)P3YqAtRNH~d1Hwj=)aI4w(@7K?`4QK_|c*|oGKG7dwtMZ>fj$^$!}la+<&0|KJ(+BOVE)w#A2|bKEF)r|#KVpZg}<)(*K0CT zQkfw!_R*p&YV$AG*yy~4t}fZXTwDCwRxumCI~S>YI0QO}8pTwip5nT(MhoXL39=3f zlph-kDkFA}uDn3JuE~$SB8~di&_7}^k+i?C(8)q%KbxEzgNv>R3dcRGj+Qk7ON9o1 z;H*@Bx6AAC#UjK!TMzv=t{?7jSMdaqsG>Iwj!3^YehyUY2F%mMN zCy+R)05;l82FQtjfelf;9G%3_H?p+K;SaB|H;oGC46ql}gT))RoW10Rgl4bFl6LsN zG%-iEoaFtS6TUfA^}fPVNLa)DA+#!g@solPAA0u?&1M{u_#(578+CFWB4XF5XMKU2 zirA_=C+ZvUyl^Ip1rne*Y2>dGRHG}&_H7!ZgJU&;zXR#{I~%z{$N&y8$}9&-(5lAw zEsSWFp3Sa_@|&4`kWCqW0Y9&VZP+<>)E5Ogz>LFx4|Jld3cx!sFor~-fNCOt&O+Z} z01-=h)YYo_ZhH}Phchel5&)1(Aa<&6#MGY?A`T+`@j^} zRJl_2eW{uzOG3MEEX%GubFi3`WJllj1`L9K#}i`el>L_qMEP%~|1U6q(^(HoaPMJh zyEuLIc5Hp+D8vfz6sU8MCmjiN5}z5?~hSUOrEf3qvp`)OUEcyn}r4kC9n6R zh?QgciHo@soyC*+O0OEzGv6Hb*Dk(y(rJhYVsmM;m}CF>KOf_G1`H1^P27ZNODm_A z$5Rep1;Q?Z^kJOruQ~jG*{}|ofY@x8rJ;vuymeFr=6X_pcfTvpS&-`1^ov7o z&wQNeb9qlYnMFYI2?q6e4t)`(({7VQs+17BS=9_iq3yH2BkSTpj1DK$sW@ z88qj3Rzf1*p&*lh<(i}0y-s%9H>VR-U}XgHDaGvY)J%TqCv4f7%A3q!_y z0Bi)!9{qpL^v zJbC4my*>oC4yB$Mc!I1aBt;cEW85Sks+g0|p=i5*zV&6G^s{C_LFy+#R=u>1S4A;h zVTZkSRBO--o#V@=LR+CHI+XZYFp1#SR?Z%hlz2i>;$EOJE0t zem~0rvmPWBd|9VOD)9sG>6;7OlQ458TYZjz07;YQ?jjyB-aPbbyOTVjDpQ}rU+*80 z4%8XhJx5gD3tdKCOgF8-YhTWtZw8NLA|wG$n2hFs(uK)5CC?TxYTMRYlm;%vrk|HH z6cWwqVJrRG)=BH$iB+W;y=B0k9^>O(bXtqOaf)Ow!iM|;#oh)a9(0;^-E1b_5QWg9 zNWLZ@P?tTRd*C6Rl<5l>8Bfb}5FxZ(9~}2#!%fHc?o#R_Pjm&0A2_1S{#|NaEvK~M zd;*?-VBi1}Wkjzsa~%^Fw@aq&s2Rys`D0#d7n)(&Us}j6t(Vb8sI%7X=5P;GKQyCM-806aJ~p_{V@8#E;9b2J z0K5T+mhFYU=nG3Ny~r;7hD2D47zE`?ByXlppog+4(YfM=JTK82uXG9s3M@!aPbT9D zSKu1;`UH@BZlOd-|L{UEhTs!j^Bq0y@eH3e?UpF#7T`S|7E94bWgU8P; zE&L>33$v?D`~- z$ww6sw^Wvttj)a5@9_?gWevKJfV11h%P?YR@nS)nDXj`>W(CS=OxsjZdF3^Z#$GOO zpFBIseZa4eaW~6a=1UJGQPsy{Wb^QB*(|N0v3R@b!Zs0xYtlnK*6VaV$&_%9?%jYt zx$rHMLg^YTwgmr7OSWw$>~F6u+B3AT1ib?4&tVkd*-V7?P9!d8`RNWB66Dk-?Qyve zcTw0MCX-@e=>Q_KM&0`Y{LS<82qdh}qVg$BGDmp}?7F2H$@c9Rp1SyQqJU18o>5Qq zw-KW#!~t<^e>^$*R{eXQm)G4(gnpEyR1`QK6IXV~u6yg^lQ9HWk2?^t6(;s%D1Ga< zc|-4?HpfE6#hRyT14&(~Bk=;_FFh=d*|-R^bknaHGQeGrQ#pJI>zA5ccXc^7JucGW zuUD}UVD6g_=+`p=j-(R@_PL$6Lmhpr&*+qh4UxPb z)x42IN|g%IJkVqX!9S1}(wZ_ungmgqPitJF_pFQ?xG140c3&uDL^$Sqc6*uZv$H;| z4gnvPrcZlH=D<%Uf8JlwcK!@LC%ay;kY~Q%{79tZabiPkr^joRTk&)(tlY9@UKJyF z1AT2ZmUXKrP!{=JM4JmgdRL-Q44Vx3aJ$uFwM** zvd6LAT~X8U-qv7aw$GkcQdv@}J2rvJ9+My7q4957`DR60pUA>A`&5k|%*gOFwaEzV%tjVCA?X3klD=YiCprlbjkYBs36ejZ6qfhUw|Z^Uz$y`> zV4LAcz4}7ZVEqI4W%~g1lFze%4;P)H|G+n&Mss1^YIcR{HS6!P-k|^K)H>ZeP zjKP8;d*A+U2akWEO-Qy0g9sjz&MdZ;k^S8>@CZJf3OvT$o%h7G3LtfOlqhanOx_7$ zuG6a2{XFR*GCN+@)D%r>zVJB1z3%NoMEK)>D4~ zAgVPkgOOez`tyz9Q1iVVajGo#!mgk4UV&g)qj|PTjAJ_wD}lfIGot5m-*^-H7I#o! zwq=!b@13R_5qX-nLXANfHK(KuqPy|z-q`aQ2sFI6Z{N?S*DP*#o9-(zD07B*bA%+f zviM}V9_NFxQoBI%N5doLmF+Zl(B=45v@3i;NKH3>&yNWgInX`sR;#|@Y7`s}+>^ZR4c=42!fQ%0;Z&LIzGpjg>=jCKlG-_fw zvdo5k>LClBB0U|?sz$ugw(OVJ7hJnN%3ah904EUQ+<$^)h`@6D`GxLi>qvxqoO?OT zv{@&GlmgrkLz&eZCAY#Hdx^XP>3$9GkuWZv)SD-jM`cx4dmqTdT*ap-KrAcdva-kA z8t9%%u?z;l_Ki4kA?2bNGFv<8$d|(&JCsjKiHQQ@MW6}%;ZVqlaY)*Cfj_kc)nLt( zcx4uhHU$QTnA0OGlcYUd@xnmJefE~}l=AYriawUDkDU>af70py1~~^vT~(|4j4j03 z7=Dn#3;aT~+%uhfM>=%GaCKJhY(O|x4!=$3whbISUxDc?tk2jo((Gk9eh6*Ug?oFX zSG_pteN?mM=JOXH!oil8s&;vJ?P5$Qo#^g`B5mlhtQv->o93XGYc=w0VJs`AvqfeF z0c7(G_vfpy>`A2j7WAI9uq9kY+{S%#*b^e zcLgvs^yD1hpl~6!d$w4=<2++td_(Ofh1C)WAdR)Jv|p&f4@ctiD! zvpXD&K2)rm`vAK&(fg#Jq57U$_nxfu+MPg~8H`nsT zve*5+(dSL%d*&7<`fxS#wxb}%L$23d;lI* zF*TA$l^__-D$s`DlQtJwvw!J(b&L|bq)AjZ;0c61GMiEBgpK8B&SCAyi&H(obM&yS zF;^X=b9emicE5As?`u*3M1n19dmV&aS`cAD#1-CGjldd#xyY+Mw---quq|rUZJqtu za$uamUbFJiI~!SKxP5xl9Au_2MCNssjh~1tuQ8xnmmqQ*FvwWakMiwN1h2tsSkqw$ zK)I{?cJ*}b+(F$%p_4NqpJ_mUO*nuW#@FWi(1z-7uE0`=t&D)!%tB;EyvFa0+F#oZ zMn<)Y=zE6sULlLgoT0c*9m#+rfGg2DB8K_16T@?2-Q`^`Y`OP#1t!zREhUVwf3UM( za}h39&{k4ru2ut4ewo`~UeAWZ%i9AJ@l=d;gtMrB!RY}7F?-~~lw>3!_OB@asM&?gsL0tsR6KFfDaxhuZ z-{X*On2ZP^7$hU`M}>ejg8ru-3CPN&67HKsGS&#jh?zO)3DE=DeFT}A<zIM?HDP zn4MlSvl(ZFz+N6)#|v-z+W#IIxLY-ic*3spA#=asq_*7p&e0w(+oF1wT^?P6 z+*rT0Ht@PM-ea8hlI^PX3I(4;M~=!`=wk60u*G1ydSFR1Ii`~bS7u_zS4NS^HGOH_ z8n}BF73TIceNJ<{LQceBvU$D(qxWILgPyhEMd4|wLYxxyRmcSVFs{&h#*m>MJDkFo zWdaLUbn&IVq0fA$T~0EwvjZEM%vkhzW&Tt&$(U4)8j&KTz4}FrPIAFuShZa{yX{xv z*IQ6amy=tV#sb_@&jLzPGzK|v-u-m*i#wT^`YcQ+Y@TILE<&SMu;ThWIeSE39m7M2#^&L;>JqLap9oy|~cMr6GnRfhuON&`&ScWy<@a&tjdf7lZ z(%ANvJwXC_z??=B#n%mhGpeS{>BX=64T`2+o6q}GFkC#pTVZb!*Sq&G_V4ThhUs5J zq^_ozWj?>6d8=ivPR=SnhyA5tswXgY9bvr5kG{lmKj8goxx_0f{YL*S z>bzys1j1Q*4b#+ykP$%|gi-YtlRhJ-s4FZ9v(G;0WtM6*!hy5(9i)Wp-u z!lB9Sc!O!lkqRx>-~587{$yB>o0$$jf$9<#y3#NFa#~;N^7znoaKD@sjYSH)fK`X2 za;Fshn6zle+r$PMhq${LUrP8e<6|Q$t zr=_r8c%?(Lyir{_u|@&{VKtC|PO*Qfkb+>9ngF}h2Iw{3qwi(+QFW(I_!e;M(&t$b zM5~k)om1Fq#yJU|<`xP##h@WjN+!Zd5h>OvV;AaEp`$k{u6%&~h=#)B*vgSJX%`f~ z4!Uop_~m#S7A8+hOCU{7tEPS^Oe#=honrz6x;Ju;rQaZ#R{U49C|Uq0jmlIuwKRD_ zDeTOsZ%BoLS&Sj!KA$SZJIibtPlD}cubqDhvIHT|;qL_Wnq@)u)IM8Mr4gB2L@+35 z?%XQ%vhX?;<4=rnhkDY^zi>>$7S9{@{!TuMvl`5FN&JbXU$;+j30B%ZUrX-Hja0+f zhn*zf{;+%BgK|T&mOVOA7hE}6ccdoMWrD*Pz2TjAc^j8ISlj(RVhUH`)Qf{p{LKR2fuD7kR?1^aj@+oH^3HZ27tC>H^|3Sya8ca?XL>N6w+prD=DX` zBYg?6-X1nzma4<^tzCA{yM3f{S%lo8U1h}bAzb|rnhlY70o0}8V_F|Yhn2-Yh0I12 z50S0CC*PScGruoCf1t7l?Ivqc-fZG6xU;@HlzR8ETf!#aJqkfAjvgfuWoGPwXvJiM zO+CMlqSxxxA|i?#z!U%wu10HF67Iw>;Z^6%0nimNx$}qdr0A!wGJi3wnjn zb@``14pRNX3X+pnfcmmE}Vc_E6D;~2yW7~d2 zIJMWsT`CSh8RM4u!9dA8G~YNWFga$&M@<5sdV}w zqT`^Betm^-kpQL_I~w>~)`@8bZ+^l$sRHoah;)kegk)%iNSPi%5%)2g0X|qVY06&I z(Q9r!HrxT?ZRBR3GSjieQ?w1sX!fpG|Ii>lZ`h2UcZ0Pd|QBgLaxWGu+W#B`(3Ag#^idXwS;B zh1xcnmDuv^N(cS&4>SSBZa|dNiHRqNP{5{q*Ia8p8wI9NCg|?z+}~~)6{Fb_5n`@; z{#Kmb0>rm1g%Y7onekCVufB;PeCvn#l#98&Fa+7$M9;ZMO>;HnxyBjR+Fnj`PbLVF zWgv3XM*0AUj~1f3+!t94+{?lBg*X*y+OHov0V5(X{J)C@J$z`FKj%ykWnuC*g%vMh zHXO)Jm+Yq+T_`nzl(}ceS0%a_^`L_p1}W){MDLM5PEkU)Y_sSof+j7X`vMd_3c&=p zu*GFojPsZTK4P93!hw3Nt*0wrzutijc*qynKpFBN{F&M=FgKCu5by5R_m4fbigaQ_ z0QF~b-3dpQbNm<-7>|ByyhQ?S;ExnDM48&2QZ(Z`F;{HGk>&E_bih+4I0i>P z$1>v+9BKbg2DF&x;sZI~^zGivV#9VN(~TG>Ldzl9l}>jw8pMCavRc#->s^q8I;NkC zLSrcMH`F0%d-$GJ+G#s{S~(^?KKi~-Q>oAIJgK^2@Z_uvHp_EinU`lCUz(Ykfpb;K z>H7T?+$k#-q3@!u*{&a`&`9UIefW30)>%5ymUChKroM(JAllg7{o*LSAe2g*yno5V zu&X(>M0D83-{K7E$FgiI8zTBC3^!CkIPAj1X7KmwHq6ZID^ng>L2wKSHO{m(9Mk>z zt8_L+?;oP~(J{9BvLA`GUnR_(pkgqDu@9{_zJ}2%`T8k}s0fT9^5&PfihUg!P?m}&Pk40{S$w0L0@_GL=V9XyP1SBO7QJOL*FH^l=2?<;iL(hvH0#U%3Vg8c~v$D7((T(9N)cU6<-0A)eP!vI~}4+8Jupg*e&3awZgPOPew z?@d2_%{z`;&G|0I)FV!N!VJ5I1FXrXrPr8NNF(RbhEPxOk4vJ*4r>59*P+UFgda|v zm%nyB!Imar_N93~z=R!cMcZMT=n)wskzW6C#h4`@GR=& zzIdW_Th4V(J1sE!8m)v+%oJoXA8(NAn`gOE7pJjO*7Fh`b(z`I_&GgVu9$67PMb6? zG|;*9(g+;sc-$Eu@Kur<6_#0R=3fnxR9#e}yM1^fz#HjDNNV1}O*p=`CtX@Bu3(%p z_Eg>x=bFGx8hu4iDj;Hj_xlTOneWzGtSk-DGo zq4Cy|*wC()F|^$UeueUZWAU=*CZIV)8yJe(2 zWuH7Z5W3!=okiSIqE8wUP1wA7P1n}TWls6-^%cC%=@H)Y(=swo2}Qtg35!mtlnm!d zVw~A$DHU!rxp$ZJt49B+wcY0@XcH0(?ot8n29Zy)e;dwXKF?3}oR66?g~EWx`G<(} zc#+=LkAD_D|K$Ol6B(nD^CGW!&Agn?I!(<-W`}`B18D&JR2`yY;lYST0{imYJ5M6H zc;!!o4lDu7_oKPo)1NV0vt{k&8yHEq9IF@`k9p_NF-bK7z(sBs0-SKi-xl}iX;Wz3 z_VQA>?RA?PV9y&x&c44WFG-|VCo#%104y$F?Hs*^E9A?u-L~Q+^!;aFR{;~EESls2 zA?+(!BSoU`gl_sfXKseUt>fzud(}A|Z0?$GDlpw$DTr^&=hixDl&l_u(%Z|UEPN3g zyTfN#E-@@k;f8UBB^R#5YAGd;Jh0@=njeZaC@J)EVt%U=94uFkRe}m?A00wVy z8-7X0e2R=jP^i67tz(WQ_(vl&g7Z&>_=rRva8G}r5m~FORfgUx^yxkL$oAYh3&N}Z z`3yRN0&jC3=iy zvKuirv11Vg zy3ki^?eZx)`^9CO=#F@Kh|F$Ebb4Y=y0#0 z2sKG%^3G#XF-!4YNt0?=P=V*w$PC8FsdxNXZRy)nn*i+bBmO3|D1CD(`cY6r< zH4IA0Lee%GWoQghB>dcb9$zLhJDfSXi)||tk*r_);jxXXeL`&RTUDR#3;{P>qNfSaxG)-T1+vG>vBGzPH-bDZXsvV54zou-x-6G7p{6{ z0pmZUbLLFJo(PAXmG+0z?VkWA!YbiX8IlKoPh+xw&?PQN1(PqeD)VDS=pOuNW)A@eGX;6W|j+@k?J5V|y?^14_%0(ySe zwh-j?{B?nr4s$jVdduvvOTwE4j6ko(e&K)&=vweh#vEx>9W1L)CB8LlJNPyH7U%F! z!+nMa35~#s3MV<7Vg+`d)646jBYnsE!Ai2X1iM`wY-5w_aTbNECjt66y9SjxDMfr_ z0SB+@2J!?tjR8gOBiE+6 zb$dC2xO;70EAb`fwLPUD(KyF;e#JgOn=m_E2demRUXWIzgYS?xLbMK2MtP~C&dS(Mm_N3PV2-UORH@Pgr3i%~y zmqoam(Axy6-VvZ#! z<8F;MJpOCPOQ;_9tMdLb&N2BJ`;^ilkRw}W1`2%(1g;Y6iA~B1Zn{0fx35DLbzl)_ zLJm&69NOGbk%ej!7@2o8S3CVWjj;^9CYb5)89w8Q#}rHbC1c9jaCjdU&lS2P`th?L zz>8!uEsjVS-71ZuEEre+G)K|+JKkT+Ofo;>d%EAD$n-wH%r8pA8nL!;t}tQvjP$|l zU!I%PM0*Kjy_4vzl6}0`5Vzht*YeAQoc6&s&2Du9DhlJ}85>!JrNL!M&ws6X+Rq#; zIGj?9il!(3JTELhN~IpLX=8(L+V1t|fLz))7`%PIUlupE1`Ddc3{pKblb#7YyZGM@ zK`k7SJ9DkPs>-+Vk6iI2xruVB2XpkJSE&;!i1ez2tPQB3#J^_HADupBawMnu zO+*U(q?4mszbiH0nhVzXEDRAOUvD?~wpSA9f9;i>=B9^6?VoklwjY0;l4|??4nXWB zmN}wJ+7MB=mK};)_Y1~4AHUv+J{lN;TIxJRRWP_)1u0TwvwO0{{}$TzzPY*`M~I4Y z-e$dII;Rxo0;L|wK(SX}Dpe+*=p3kkaF=%GlFP=(hv(dznXvf@SF!e`R!R4Ja)aYT z-lp+dlNE4Fse8ov;U%>(Wm=X3AP|eR5_Ba_nrMNRhc58ezv$knb4T7&a}YagYD{KL zwt~3vflhnvTSxw6{70!JVGrbT;&}WIOs56Pr0#|-`dbfHL68J?_8V!ZKzKSQDtrk-T-i ztqCnXt$YuWR!D(IB7CH3_o<@&Vz*6aH;29Ua>3P!rkwmo-1PRfSM12%h%?c&^q@+_ zE!_;uyvY*TfcK z<4u?H09K6OZ!qMwB}e8hT+4KZ6LT)5-yxgJBftA5-wL<*&7{I;j>20lkvsn*Y(6-;Gj}U~18_23tGbCO^}BoxY(uCIAZ9gLhJ(xQTP>!BRal ziZ@KQ-A@^qNWMrAN4PZqK@V`;)3TMpAf7(Cmyf`{Lc^lNwdo^yDSO8Cy6L{S*aV*9 zMc_TC;LC&;n-c?VQsstA6x)20bRzY!vYb0uE(!}A^?`Cd+nnRAD@BdZ>xBa1P-5JW z2!`FO&b!!8^Ww2Ud`fVrhR;1)V=l$wPk*)HX-HKj(gEro6=@SA>IYT$k*(HZdl69G zpbZR587Yq^ET~~nz094iaG(e~DiAXRcAkE0T|p%(umUjtLhvhAnDMGqt0Uc~k-A{( zDcEXn&*1RX8O8U-oARnK=U5bxs=8_8WqEAwDZ(@)a+L*Fb4i$mnf)-fsHPs% z+Z+Uv7X0cmDYi!D6MS27W4$+Lcth0LcX$y&Td!nj{n~@`4FjpD5z1T;+d0O$Px+K) z8u5OU7#`rq)!XZe=q)?lu39n-dnFci)3bXGPG^ARO+nYo^{6p59Am>$34hdir4Nd_ z@!}>}`#?n4js20^bD!qt{DQcRv;68FoTsmIsA|K5b!t~7TH;U9`K?d{-6U8owHHz- zLStIj^`=4_v#YY!hlI4GeF+K;(~oe=tn!&}vH@&K1hkZ|ieuD1qz02O$LG%dB)ZK} zLZfv1zZ%xN`ii9+avc3m%+Yc3FwLzkQQ{$Xs6lJ@cHQ=CP=n@Y%$K~b4B3v417W}xyCkvG{$k`)Apyt zot!|6kA(8y5ptf~b)ogK#TVY;o-q-aX=QH7IZpzpw>74J^38@%@8v_|{7l$+(uWvv z7B!aJBwO~nim=9rZ<VMSCr()7axryIj9ii40ZNa%^iHu65XO-p0>MI8j;deHgqpOL$V z-=8DmW-x_S$-xwqgwUszvd(@pX|3g4Y>qK2GxvRt$lxr8qf+wbf2lmfh@g5n=4&?z zTbs@M&Ha>aH)5h6nbP%8JQ!Cj1uO)~KS)ygsril)&e23CN0bgDM1tXHnSLe#8Gy6O0uRJrjN>Y+20u? z^lN&z->oqv+7`OA@7UhfX7fX*W7>WV-;1p9{T4bd;Y_^b>koM%%250+2o(AD#_qe6 zsJ`v%m$iMuu!UkFly!;3Kc@Mx?_v4e zKRgv)-`=k_%B=g1U7^r1(#{>b!8AYAHNcgNIC!GwURURji3Kz4LPo`A5>n_cNYmU$BoqR!!*?%j5pXT{W)J(l*{PIZZ4 zK4iPmA{5mK_br$v1ls;X0J1AiN=NQKc>0(ox|@Vvbk79Ae(6Y^Rn~%jqMh z%$eBm%b>C8`v&iLQk~WpFygj#F|TldHdo!dl%<}sdeByGihBTn$2F?9aw+!ph^<`K z6gOAMR^}~6ayM$D>h`$XJH8_fWrhQtL%O@K8wozDmOQJyeP{}pKkw-X7<`p`4hn?k z^nN8`mwkr4&8x{aOw=Fz3!PSPQv_o;aH9*SK5_GMnCRt7sofs5$sKLcai}xSEZp--PQuIli!A zBWFetXkzd|KJfMa_6xRdi-~<8vXPr{>9VAG3r@qrS+%V$EXP9R7|u*rlINW@xpnyk zlC(qO?+4z=h~H*YaFb1Nh7DeGJDg5S?Aq1!TaUPgSUzDp@(h~1Ctq@r>|wLDIfdnC zjyKkzRf#L$^{1(wF~Ae)GQRuUdnlmF|F)HWTr+?~==#-9t!7i7U7l=+r8H{84;ni8 z)x6@}sQ-pfLo|Rh@KZmCOwJ({{3Z9p2H&hxFe9-1+P~TA2#9_{Lo#XW(7L3*sl#7H zKe|`f%Drz=D&<)<`PC2we*fW@qWm#4spz|6{0v@V^PBeA@^9%==Eq}B@>yJN=HE2K z{h@TS>0l~SyF}GV`Oowuk^TZx(#9;IxV1cPVWVOshMo=&@2I_C1?Z5!XxX%lYgple zBESGe(^j>CDb+-0P+b7EQNunIcP@5sF8PvbifA_bjXJ&KuN>%@o8|X!ar*qv$);Se zp6@;(n=ys|y?wfsf85T-N+FWVlLx!6`APJ;f@YZ=6J55_!Pxffq+K?x=Q?D)Yf6@} z))FURiPxD=$7Sv_;(s!hC}rc@quvJg&wm35UD#&b>9PbLhlVo^v$+IVP!=_N;c%51+4}9W4Bl_Kwt+b)(c%#=M!RD0Kly z%Jm9(cX2YepDA9AW$Z`$#bN!I``$Ac%R;ZbehpdUxS7K|%14YqB{=u{4RU)I1-1Jv zBV$I=!lBpiC*z#K=d%pj;f;SELoM%nON?MA#Xyq+aue$ij-c*l%mM^yLLO9wHFi%{ z$yc!=pQi-26#NyGarzodLd0=x&-%#h+9U`5PC#3vJg`o8HK2O%FF-jIn&-A1aE(LpAtc`R!p#a8r*Fs=R3%2) zNVZ%(g~hZchiPXT+J$CnqlrAUvoWx{&Wp@=fq7T?G)2>+X6%UVQ)@|R`$Q7=XQuVBaXF#A9)8X>cV~KsUMSg z3z4$>hDZX#3HN5WAS8))G^u|`-83q3D*A0lOfKvsWAabteQHK0;;XuIuD{YIR3=@S*;r*N?2~ z7!K`FA*y!OY(>7t_}8p(I_R7x!oY6|au!tlF_7V&V3RqUv?}?+cNT31rEQH0Pnh`1n9hiW5bju|hAGV(Yf%#91sn zS?|_k;?3gL-U(jR2)spu^ELb~B|DKWR%O&7deW?P_qS*x$YF1O6>CHH*nP%C9)ouX z4Qe4nrYA0zMlm1L@`-x}%F{GGIM)Q)UtSL3(BgX6iLP`H0rtE3C-7O;@nHyEgi7#f z16t)Z6}jyxt+GJ|ISK`OgsDVUK0r<`q$2uKdC!}|;hCC&T@?TGqn z!ih9Ek|#pSE%%Z%7s*#uDx`?_bvI1C-ywcb(bJmpL&5sq-{QVAs?m8d-hb*8BBkHa zpIQ#fbk_eJ1o})ksc6c;x}KBH;g`j#S0TM*I-#F&2b(tf>ycP&O0~q06HUw(!jln> z%*GXC=v6>^1sQ?vrbJ=K@o3z@zq9R&?0e9CqyDjPxO8@mmAkKX>pYqYsUmU7eOqm^ z=g5kcuLgtgYjO1PlV3#Y2Sxo^#7n+oB!V6uFB*^z;OqEQ&2cc=>bWD*5=Zl+7|Q+Q z)m1nNME4k5cZn>E9}ONipNr2tg)J}H*sWz;25%UKtZ=yyTL$W8kNR*9$2_bH;QK>{ z*0Tcgk|@}%11$qG5mj508hOb>2@6F=5k=`JtBG~890#YgC8WNVi8;5$7yKp)3(wT9 zLxOw*xEhRmenWJ+0&JXJk}2491|z>0UPKb2Os4n3TBMg4{d{)CFx(B_0 zLkB=uF@tv;++bQ_ry!;sDqg%3_daAvDLEdcNp7Ep&iL^KX#34~-xV-=+@O!#@Bh@0 z@sbs>=t3n8mVe#rvE~G(rO|1TNUY?`1c0YX$RQQq5AetP9E$5@7a|3#(P9U8_)X>r zjC1M0icCb#dzqee4IKMPiD@n8ijFnEgQs%vO`Cuwj1Nd=i@cM7fswDi-+O- zxCrM_#InhnXHW45A3xeR($v~gO)y1q;Qbe`q`A+Ci<1UGO-$=wFF>FW*ITBHiJ2vH zUhTm{r4Q^wTTmqae9O*0&?BOaA;b_r{9N zAQEsT%F3vJU=50ytpm@cDfX{ID8{bQ4Jb!`C=+D(X72zOZ*z5c^vN$1w=uZq9u3xU z=c^{yvI!4e#7x-`+mn_J*c_e`ldMwe(I@6`0f1+6OZ@IqNZu;CSKK!heGk59Jgv{9 z#gBGUF{%40m!pL2IPmK~iivgXhH_M;`HQP1jYqI+xm4S73>}3$owXZ|m+P^3SVB9NjVH6>l~kT-zTP>y65StNEbx3l zbN~mn#8)(CWsi8(4`kBFic`{URB`%u``Kdb62Z5Tb=u^+FVMoMi>`>WKlPv@rJV}g-P-!dGN^X6FX%*#yAt@GJ@2Kmu z)rj2zyK$+9aN|~yd&pp1TpPqAtR!Jo1FPr(%*5tkHu^ndXc=*sC`>^lGT@O^7r5Vp z<3()Q++3!gOaA-){;$HAjf-F%LDiNdTTKEV??AbkU({~<0j|u%ssdqZv2O;-)X&oG7H0q@=oZqM#SMZPu~LZ zKHSrE!pk3l&Ey@-6nV$jy#YKsLUbGRLlflbN57d`w3@+G5@=*FQjnDZTd{WU7hi~` zI`OJ*l^MI2vtrcg6U+jNsg+BZdsy}~PS&Oy^4_{<-Wy)w-?mIlU~ILI7z$Ofy9VOb&USI}nS7KqrqSIv1QJc)3ARz*Z*1l;Xa|RT;UFZ; zrA8;0UTj4QhTw1_vh{R17f1bQo92@zSrLKGQ_Bn1?&e<(t{|e@apffahne zkl?`VPk$2T#a@Sdi`LI~c0F>SL+nNs^2lyEfq?B$bdA&g+F`%RFqbET|BCdGka>yd0e0sxpMuzgCMxo8LS<5Q6eQ>J-n^@Si$mcVdcar39Io zyYPn%`Yyahq91y?^aE1q-%n|Y8)Mz}_P~n1CvZ;c1ju+1x_O}O+~1|GwXP37o|ahD zv}C7IOitl#Pph2vq(%=rJ7%R8$qez#FNALXXBgQNd1l>(=GLM~s2iu!{uMPeZtScAXB&s6{4qmZ)D6>Oow_WZ902_b1xV{QkILHW1<>G04QzQY)>6cW*BJCeLe=yajL9 z4@ID+Hhj4M{sz5y9xYBs1f^gfXKM=!QA*iM zR~cp}jgg0igoQx5&V0DFZ~4MIY$X}z@7V|BWQkCg0C%3jaT2zF#_js zCjf((cBZrX!%aLdk5lAt?$<&OaIcJit{E;8?EAXxJji?~{HTW+!{v zwnzX%QRH#nTgsxtfx0u2ERH?H#}Y77BfpMMM#;5aU!1`9#W*%hXW-L^)6FzmneK|% z$x{8=Fadry>N|w5ifOKC(+shER>b`>pI1y%yzS1B_MX1w_7BY+{}dI2Wx&&_Wexab zt3E1!o*9LDo2^J%m$zKV?NYEh>K+R%WZWD0Pw!f!{P?NpZJvK`X3|k~9pYl1%b=m` zuD&p`&!63TXV41s*apn8ybbay9-7iN40L8|v_gcubW46&23jzQby({nJIJ+%{ERdh z6!Yf}%qZ50iL!pjB173vV2SPQ*j&4dHV?tjH| zF8^{S(Ig!$4`X;ZCj=ZakRT`{*l|btl%VRh(s_}ium>j|q^=6TNt%sxuRspssn&c_ zE1RGOil+Kg7D~K_ZSZYpGZ6z~7tJT4#<|svgI1+X3$Jg!47w0VGfW2HO2s7Z7ZRwb zI8sk5NSja4c-#&a`(&_0rGfdS11}L|uweHH`eBA$nEM1t-P;33R{7UsY38j>T68hMv%|cq$T%2;3+hdaDI|}=X zyE3+1r9NyXIr1Qvm|X%yD9l!KZ$){$%Mj;14^BeX!m6?GOAmX2Wq!7z$$-xo#4Mzr zRov}HyyaJHQ=(p{Ux$4`zkW@BIdzU1Af=DS%4i@xaJBYx!g#CcXH?I0OSz!9I z4!mDSZIv)8Wx>Ra=rFZXXhO>xPC9@PDoF9QB6vnI@j{BYX+L1n9Ok6vEl=n*3Q~>r zB^FIqh&X zvw*`!wuuGd&<(g%^F?6m2v8HYo}a0s;CpvT**DIVX`xRgubZAA8&bd|Wf*X=`XMMv zliHQ2`X|)8?Wy#@?EH7Pie+~6htRmx)0Q<&-V@?OspuQ}47V`_6NLhl{PkJ|t#Vz( z*Gp(nEtxy=P#2aJnm+MvTkOyQg9yihR6_Sl(#o7C-aKHhZ51ktg(NT;0YlA7Z}(;g zf@l_ga+7R4K|uOcALLy3^B&Nd`ZQwf&>w|x7)PQnJtT+}NCWTaEf}IJ53Q)pas$3` z?UCJ03D>53hg+3joQ#X8p!F3bqPH;zvj(Cf_LVqT$n&n7Jl1pkKi<|qIh+BLVNMGF zH;R>E0tJ@j7amuipcG$}a^v3G78p6<*L`TkaymsDI(L$mBm+S0miBB9a)9oY*5Ibt z58#BrpiIQxW7-$jE6fg zc2Uws=5In$BldM7bcEXSQUs+yeiNZvQ{#_8qiu-Fe(^5~2`(H%jcQG-fN@ zBv+7-w~!r<%_nA%#IH&;8U?PO_+GP1bNXB4M|{xvrV03N?{`Jgz7|dElHP1@MnTN9 zID+p8GvQ1r$5pf-jf{tkfGlmtzT;^3s!7}3J|N)_OCLT|*QG5EuT@iUJDnw7?`QVt z9m`Q0?Lw*AHaj1&8(sORHA5T$-m|iW&W7(gmEEu!FY6{#zUP;XX6KgumFL7WieE-_ z46}Jv)_yR?`LS*R4g|rx-~SJXJMLyOtJ#M_QfWlqKUihHKfX-m@7I~*>l}h5XcHg> zh^3)sv%3&Ewej6!@7$maJzIv=Usm~!{GlD&Jgl6g=;_0yT8`$5(nh_hxMB|M38_P2 z_cxt~e7kSq0)-s~FA-5`Gve41_PdMDV<+UqKT!cVnVL!1~5-UEP z=iKj8>hs$dxSObNsNy5Kx5ku)B7iRew}?oscPZ-x$moM2?ByXepzgONdk3gcx7WMC z`;1}Kv$g~s@|ATS``&hRpA{*Zu;5;14Vo6^=1GeKV~|kCS=UAuJL~^y;iwl>LO$o=EvXIaL!|s`_9imRdd~kKzHltghtm;Ej^15u z7)?ncW4n?5E8@YUP;_TMI^1|ZE;m{ZiAg2V8f0i$gs(@<$)vW}3L2HCY{JfV7D^ir z8upv-A2L5OHY-Lr4llen4>u?p<1qkHlkLZ)r5`2#H!jWWPrhxG3)DJLuS0N- zU1vMY{9mA77Xg0LkOiyZfaG}rPmw*dE^1aLJZcK`6L}7`lwU=r1=+S;OSRnh@)PnewEt#2Iyk3ngb-=!;Ag|H9~h?L_qe&gbNKb_~Su9DMKifvEOOnSVq)Rc-!S?f>9hwtEN<6*yF!Bw z7RilYLvA1DNF%0~LP}A*nV~GI({C?d_taM$z3YAocr2&=ALOu|cdx?35%D~_dPf!C zM3E=KnuHnwmh!a?lFktQqt9Zt+Sk2>r7%FiVpV42FY|gqC+lLe+QEMaal|+6y1;V1 zpaLAXYW9{!%SKOT6oX{QA87Ej8{HS4=@*jJ-yuDRF~13heRDBC@C^#I?_lblBeiI% zf6pTsLk@ztEqA%RXc(?b_J!Ub&Q zM66ECj>gFJc%2*BCt{cq;r9nN$eg4jsv+YiuPxY?tkTBZ$~+o1Uq$MhVraZYwiwFd zu}wMs{oTEY>fm4WKTYxfpA;K=x4vL!d;FIbFS#f{boHn_g!xk2_x&N1XGr4u4S=l8 zxqc|6e{cEKTni`HwBk{Bfmi&Q-0hl!+|CW-yF5DIjfz4>Q-$7Yu=iSySN8{{eZJz> zj;CMb1*H@}to@U1Fl;#)gTB8B^bYIft zw0X4gA8{P?JhRzBC(S>}CRxK$M~wWBBiDBhF4uGr)cf@5lNh)h!@A2Pp+w^J=eOXo z9gZGY32>1?VM_FT(6EqvBO7+<`1fw;kD)Qj4IAN|D%n<%p@d;Tq^6)qtKw%Ydhxr; zjP5}6fepoPrL@}mhV^RR*3!6x8=JGp&d}Vs5@iJiP9gkcqSl<%pn#VcwM1Tg47g5G zsH*var0MqRnhpvDSJ&7^tj4LE4~h8NxpH~)iJEuD!!gMfeI6^D`M7~WZH38t-B-_} zl8xe;l9kkvZ5?z#0^HwI^=oh7Z=4Fd|DQ^iA%avc_FsbwoIcAJDE1Q4Q@P^!?|KxcURT9fqAe=Ds1&2LpFq(!j{-$-26CQ9v> z*L?DQnU3z>tAD*Ow?2HaN}$+}0$x|GDNhHf_KIl^&~t1xrsM=Qbu%;CF_jH7+fzZtRvQ9gFNd*`12 zNV3`K6e82O`@$5Ff_$8zaD*HC9RUiajCZ$c?Y{t7;qj!`&}P6;(J5YS&xx#@)HYiN zRScz9RF?oiU<)C#?m5}N+%#UDkd^T0dw=p&0@@}RX2ds4<2K|yDjFMXF=pNtSe`ftm{lw%{^5q{ESAFZLN6K)A)<gh)z?nc%G;Qe|g*!d|jmlU?ikrTho8 z4G`FL*#g|~Q5T6UqBXYY-FsgD-f~O-1#wMO99Sb1gk|<>d?{ zhdlddh}TbW9es{|3(cUkq@9mbIeENJUcX*$xd(SkGpPSw`$HjCg!v6YYMm;y&8iQj z5MnmYh$zp=d%+Su=y@}e4~scQA$is670~l^!uskkyBk<>G~>u~7t_NGpCl!k)i=yn zYA@0Lf!~>+`-}D=5O42r<@Ld&c63xNW=p*o>p9>x!ISX6V>11_|M4Bd%emlxKw}{& z;{Tsl%%Cj+$s>Z{w5()8IXvL;U!tco9rLk`;Tz5| zwjtqw$Tj(|ezdr@d}}HKaZA6UWQ$A0^w)o(XhZ$v{?N-sCRSY;HJV%oGp71*4KyWa z+ZNn5J+a~|X}|7q4iFU(vT6LDj8g<%3{hmyD7|b-OS0ADyr87@1cFs zg8eU*@o?|PX94W86~!v1B+A`!{(5nk`1M{zo^J?mbciwUUyf6^Xi}oppgxe$ zuIo0pqcq8MP0$PKKTP;n26WD^Q~Vt!FAKwf%|qGS3*a?yk8je46yjFU-E>iEYX3Zn zuCa1xzVSC9WST>z3^3^y8?DpvKfo|>m*17b?36@zj=Qi|k6sjPF(xjh9Q^-*!f_T< zgwq42=o5`~nKXfxDNX~8@M>P*i>2?@2lk8bU;~e>r0!HvZs7Q;M{p+6b_X`*%`^e+ zVAMr;V(TsO-#9T-hxeB2vGY*-EcB0=74;$i@yPQRj>ETe#|)TK8vRauh^qWTpUUK#BhYYf+-)7B)1N9uA4|4hY*s z1*&`YhIE}hUS$<+e8K*?x%!7?$dgny8olWpf;?EvkxjS=My&b(ZWuYlyc!D zrVCeocEvBKGKZvzw?265#rrP-i_Y)@CK5Es2GpY!R4ZL;Y}6PO>1 zo)xEm1MX=3Te<`_0l?sQxfMuIRSEVl)yHZY5b z?z4W;Cowl(WnX+~SwzKd8S!7Pj>2ZQ7ZH8S`A1|dYningSEg%&hIq!8tf@oT3H|F* zS!Ru&hmyIJ<>h;yD5z>W5gUC&?GJwgTPpeBDT>E}Lkt&$c zqZG4mgp1vs8Sln^SaXiat~kn)DEOE7Z$1!GE?m)*Fl|U>GG!YOqa1OBSiDdd&MlC0 zJ4055WcF$xcP>2WZ-Yuqun?tf&puh7(RI03Ya5=XH(3hc)~)c)Tc-ZYdVw|j+o9B- zs0p**Fn$%(w`^aBDlfcMvt)x@;<(_ebm$Y0P%!fn3VR#kKO@R3Eu`AQr< zUr7|GK6_Q18?N?r+Wq;>yUM&pnUFFwe%LdJ4oAhmckgiiCh7EF;foXV?=Hgg+UaCF zH(pEXdsqspT?whNfUubZ(DKyOZ5TPWxpFY6!S>bRZVz?Gjo5iFY9GOcWiZN4-wThF z%6<82Wd1;`;@$}5_qD(Mcch7W9%r$o=K^{8lK0L8$mz`P8PofN&LI^JZpI0PoO7hM%kofm z*uZPCaY<&&o$ue5bjz-Bo_vV8KYhFjd*MOo7@b&ZS&2Yhob(^37jC-l@UY;dg%O@J zah>1KCQG4LKeJ4f1B2-lpNq-ap~?S+*4Sz^bIZ8m9>^vBso&jQORsTG&I%aUHkKRJ zlde9K{02G7p_?I%ogU_u#^3(EtKvDdgL1K6g)EhR?5f|dnd;d(GNIfi?9o5l8z!bF z-tGhyJgfe1|0{{$5(l%5kF(9Q0qL<&Si$S6&!z@s{Q$g2P#xQqWzfZV2DG=P*+!gD zOkj(9zC7@5eKhsApFpZ|(;~V@)?S|wWp9O7 zWam?~-}B1jhpNs9+w6v2J>dI`Vaa}xBY?Ji z3*81BaWihE2a48g>DF{c^H(cL5F6@L_`$n1`K0WMhK`>YZ8|!nA4n#xb$9yW+icAv zAE{{ub*;=tl*B}hVuZ=i^U(;gc05ojwu#?!#_-q%;#H&0FN?{`y zoxBCX++Y&|G@Dm9LeeMhC33*+CR}4Qgf!3u_y2m}xbk&^|fs38>uQ$W%|3I+7-N_Hyv8W8$70Ds;;WiA4lzzL{uhA`kE&7O0Q0x7Pt)6VF z?Brgv6A9OSVeA}x4@Mj!|BEt1+R3f#K-e}i8yjnY*Cp0&M*%=C$m zc2Dfy5UpDy2@gdaPD515e;FEzxd=S!dB_xvouDT=UGp&W+6+UmI$Udl%Mf?a*9J{wFZ76| zUf{AkIRDb5vmB4CkisMEkJJ!keJ30r@PiN%IckA<3 z8TT`Zufswr+8DKL{naSE^`no?`f^3>o7Gv;YWM5Dap8A!(Fpw9sb37QY1a@K1z)Tl z6^r(dy6`A@iDnCVCFMPN+~ztxFF)>8Vj#I|@ zHl4u?VNnJ|MAs~_GF>0ChR>j9)>@u82+v>_k`YCZiXIydhY|0~pl(52bW}@?`%qOC zA#1ov*xNIWOujlXqOLl4vDM4>R~uNKCAPrBznkwt?QcS8#J}i(krgP9#0HDthMGBu zsTIM!&E)n*1iNI8ddj50_H+Cv!E=R$&ex+qZkBSKSOu*V_E^V0*tB&o78^vHq7{+R zVLj94;{*8~UDW^L8~LeEie{G=BRIrHF6S3mouk~a2!qpp_Fq%}38mR^JT=$hL z!b(*~pmh9HZ@N$s3#T`Y|E&nr>y2c#FJyr9^2M`#8-5AmkHi0ZUpCV;c47+d?lB>; zF*o`f7ntUpvUgoK3|Et?HcPbozaPE`XY09BnsNBAk~MT1wl(P3Xpzmr-L@j>>ckvG zKJNaq_N)9Gc;MYP1MG2w>VHj(G6w0Fy8;K#RdNX=+2@t{#i4bd58fo)@gZdIfx=?0 z_A-*`6IEZaJKd(vE4F6C#{k`+SAaMiIqyX_B7L$Hen}eR=O}wax2@l9cb-JR-EwPh zXXRP>qmaqj>bsYzv&h)c$<6n}$)eqnENQ9K@h)hOmBMN$DE$ExPw4e`71-blcEm8V zgLd!pkK2K5C%&`Ia6JbH&TZ{J;70^?<5Y#{1IE_hVD@S8dKTe<+)R3UF@ZD;KlJ9a zk0;lPE#ZqXChS65!;11khKSik5k*$D$dku7v|P8lSj)17k67;lzBY>mvw6g3?Z;Q+ zbcOei%ynHCV!Ow%48;Z05Z`KJW&42>Zkayn2~Bo3^3JO5HQn?ApR6wHmVD>!tC zr#Wc-bM^eeUnp`7!6BTba0#DL#-zD~+kh8gYg)5)hbLB-%{ z4D{61s%8Sx6dZqVj?P}tU(eSUO1!W877Pn7C73q5MTr!ZRtBhW1}It~5`;0Cng-vA zDw`2KYo)8<6qglNLjLNUJJ+MUvwSEEIw%nc&?_T0BOO8041twp%N zywmP;0Az_2drsvmLmHVrDF+Q-PIh0Mt4{8KaP$;pF9UPMi-6f8Dn^!5 zstAB?ddp7s^?5wWYa503C6|K)KLZcsd))p(tzW!OXrlZ=VUV+u{B4C`XAqWT>`tEx zzu98243rb#P-V0AM59c{H%;<%ZVvws<`>N_62inX3pmo2{$@?>PvKBYFXN1Zky9X3 zN|GgetI6~oXFBxCkdb1DQX@x2#Z%ETnFYM6l~6rHm&?b_vdbA*nT$sug@<8F2x!--Wft|ITI%^0RU>_X9?w zOeQx(#Pp{eGjD%vnS(!3ORIlRvp;8~qL=2mFfVhjJEq=lsGvwni+SS&ex;U#Wo}fG zJI)GyvJq^|@zT3sncO8hYmpEBTzOy6v=MzIEzEEq_AeO9ff}|Y=>+cF?EBTqC_ihd zRLGGun)nmdEgR;%HBtM6(d^6cB@A#>=GXvMkNJpMT~5tZeby=_`(aKsbc&dPQcq1!tLiC;C-6sT{y&J(`w)(vxG-fNhxaD7<% zMId<*#Ur$HPwt?>S1?Znj|2rE_YTIUY@D$cdxGUQ#<*j=#Hq#=Z=U%IBoJ37;e#m?TSQMQAK31TF35etuae-a3%N zN>z9_H&H(8MkD-aHT&HI6WuTQo!6apB4S<@megzhq=bP&Ux5vcUG9sw*Z%mM;BK1g zJUxstk8J!=F|-1fkJ?AnCsf^m*D*oG}K8Y0mh+Bl3~Qg`AK*zlClL|VQ1)Z05fy-__(5$W+3tg z+thsb@Q8KpSaSLe#xSqzi8-uu@6iU+^WXob`wy5SZyT&I$ zd4DFhq&Zg?z7K_CN=>tP)O`*vY(eW}y+Nsg>|U_aL)=&oUz(fW0pU{$UQI-MQP4l# zbB&*Te9NjiKyqtHHlkWpj)dh8YIoJXz2uVyf&O4U;fa*!egfqyJK=*7Efaxm7i6Nk z8&#Lf@x<_&=wzXR0>Ex56jqt?!YS<0F+|v^Ouo9(lNn4*u8}tfpxGqm0#;{` zS&(oG$xQy*96wQ!s*2A3N?D6uv*C7(mY8uUA^DWJmi5b)0BvlPATfS@qPHb~#vH`G zO+tPjRSE&z1`O=gx3NXfDH8D?x`V8c!AD!^X`@PY6q3|<@pDKD#ias5OuyloI^gDJoUL%kl{K3`c zRJG=qkNM~NQga!fp)s9b^dy@Y#U&dLTJ>aBFJ)_iZ|kJ36r=B9OT&E?sd~}%5~E$8 z>)l^=m6j4a+bxiKI0cUmm{?ia1d;{ehT2#wiX4EFJhV+g%#gG8XTXvjyU(l=#w??f z!qT_4L?bo>*@x=3N*%4%7FMw1Am($j&j7g@+z5eVpNfqjuMHA3hwqpB_NA41nr9UA z%k@~m`8?zhYVc6~>Rc<;Oo;|+KwgJS3U1k|Z^5w!HVsbfh-0a@YhVsS_BJ??0G~_S zJxyBE9qjO+gZEGip;OPwcn%K#SCt0gATG+-M)*dS`~f~!hW8T^8QmlLTuM}kHbm_o z77r;_r$KWVKIt|(U{Jo`R1xfeB?qSn`P&*$lENtHIrGU}h^v+F@K-3UZ!iaiIV>)V z`=9K6`M_{_wBYtEw_*xK9VilfFSVo#Hddh93_e`E1iEs>5<4{{bUKebJfC!<$ekGV zVPM^qrw+|tHu*S8(_&fMRVipJQa&+%OR6ThJuj{F@T9CF>TZOi_WeGP0pwJEP?U zE=TiTVbk&Yo4!OZo1+J4#2}SyO&o*Q4{RL_LYyM6JSu=oN%%^g+_bsBW%lu=HIB=p z<`C{!Nr{;KoH`EuaTTt*qfoTl$qYaltoMs@gFT4|q3tDBwLC*PIS<2BNUG)0i~iG- zJ!UaWh=civGNOH`BOSM8fdQEWPLe7piRqcIkzws=h+yf|G1ZTk22`a07A8EsOnllr zuvz&vGu)u#t0x{^&SHrkuOL6_0!$`FQ^XIHmc#Hc*}A(dDJV!sy8F)D~v1j3b!L=MA(&ov8WVE z#OlS(U_PROGh{dj14_NoZhA^cN7R1sMWwcr7Cmq~qtsZycRmf-4bp6c9T}Qbg_jJ2 ze9qeD1(7L?tnOHs3P(As0YGA~e<3|9S!&uDoJ&cxF&yJB#!N9V64Wd}#}zeeaX5$x z6_u?x^YT>9{%TzR3%~dq-3Z=+`$~D>wA~>hh+%6NNryu7STwX8zF>42!*K+;*NnlT zuh|?HH$Er~NqDY3=f0RJyDV`rpUkl=>bV;%V>Vg5MJoE-e1TT#2rvu$o`?zqHr3$n z?wMVw8RQ?Va+D~)MQot(A>lOhb8H7Z-FF*0M5 zb^yPC;0?NL4F5P?ZDYlsZB0P+c`w6I{SDs%@{x&8UuJH7Go?A}d#ERr zQ9yuwn;728*sAPAQmRr1HF+yfin}0qsZaf>k&-tPcK*Y^%RL73z^)!XdX%su6k-03 zLDPIhX>@LUNPE@Z4~BDk2J#op3UMI;qp?~og(g_$u#*1odQm3H z2Y#0pQFkaO#@6G#V+akS=BKpJL<-5-HR~tg}#*VfLP2qm{rX}5(SRiT1_$%L+7~+{h+q_BB2x*&>t`R zCBaquys(lLDr#~5%)e!06MMar)S_LTC@x&d_snZc{c<9jCk+M<+MsIiF@FgntGF23 zMDnby^BuX5__K6FGfl6zUk^FWRKjgp+T2@|>P zgCYsu8RbcI>iBApEddnI9^a)`V>;Cy8fh>S3wT^Cl1$n`OvrzFFEBgi7mhs66O-?# ztK?DGA-G&eCtCAGi-Mm#u{SE@DHTO76)GYVHw@zzWt|oU&n`!vMn^$b`^t?H7T;Fm z4r~BYU5oGRXK%V-Q8nitlBu8QSphbt0O5v4MN`~T!qh}r3<3Va&lIN0k23lTIxXAm zPvx)LC$egbj%k&~=~`0!UT4|w4vTmUpU+b*J9X0vqI_m|olc3u2sl{nUPCZ(c-?QF zqwihd8a<*}pxe&iXoR9S;><$e%%cn*N%FRFGH2dC?iI={$1O^U{$n!3cET^F+3 zRTZ2^PRL~qB$Q=prG-x&?|5iCq)?9(6|d?9-t7h$iztdKA7=aX5#NsL3Lx|TOcSC7 zK06w-vXEMx%~G-L+hH$~AAv&Gq<(Nl>*(0G8l{TRY;z^Vg}r)suZ|v`9lzUo9QOc= z{iHl1vmt_Z4oC~LJFGauhmL5%e_^6Wk#&d0c(U^fVL|urFAelRgP!^*&d+LJcq{rE z1M69io}P5v45R#R3L-QMC|!Lxq}rM+nyZi*Qo*+VB(yc*p8;d-G+1KT=Ty1of?bJ?c3(hR4~pC@Q*EK3LVs&x}ff5ebZf^7F9F5k`JPb(8FFmfCvk(`^>{@$Q9oJ z^R~$_mql}DjAJv)ARFhRp9dQX&@-gAF~w+%A$BdtD^bVywKvv@BVV1p+l~P?9=+A| zm>=M>@%`xg>%(=+KAIr!Kae?(Dxs!r81syvj9U zCSRjBX{~I#Ti)Z;+2)TD^g%2zonzt(8@oV z2Q18uW^nWcZYhR8O7*&g2ay3;$4J=q&ep^T*+QGU^^zJKQ5n{{+>*K7lGx_c!>>Y9 z%0I3iQ@iU1 zv1E_fjR!_~5?T1@;Fc;&8vUX>vBF9=xg);0{vzunN$F{!^=k9oE;j{mKQ)gJ)iwcv zVj>L@>=~MCnD)>+`7z&C^vo-2);5jw9r4Ws_{AGn7+n=2U|_@IEis%o^&lP03ga)I zSh3@MblD7f%kdfMh2t4L!M7D#V%SKw86TxrECm)n-HJBMFF5E(_BJn%6f?PW{qL&O z3r}5(sI@%JueU9?j9CCPgKaIu26-5D+Fh8r4J5RB;_N=da6#~e9(v*p(c(_! z!+Tu5SG|p^?fWQ%fga|s?<&Pancr1#2SQ%8-8?;*+Q4@`pqd??b5MQ-IYhFuMhMxJ zR%-`O0R2Pq_~RRDE=sN-Rg9(O|$NCKQ_Y@zN6N9q0WSk~~Nto71`#wyJ z!aCX9*kklJn1;N1#B~ zJaoFmY8;Bms~m``(H0hdW1_!wK&y2x51N{%la)+8Vw-k!p}N9p`ve-bZok@99>99v z&+1Wo;?Ydjgwk+lSZ{c&dcEAI^InqTuD@HO3tIX$Rs{t;RwsBYa_X5N!YZ7N4&A3< z?1k?u!;t;wn6Bqo-5Ou4U2W}LTqI1AFOUw-W9sNHZ5==Z;U(oJ+kyYi-tf8|yGY8B z<^0oiBX4qwCy~jA?{hB!IeJW4o%dX2tqjPUWC|UDbaN6aq~6F*x9ZXhiAJczj#JxE zI7M=T;OdJe*&nzOwdwwq4JO&XJ|3ZZqqdvGhxj{EA;T^!HknwypdXBqZ=Z~H;`Q1> z^i7ri9VY=@u49MgoM4&Ka|l~8bg?8iFWxG${T9jizU7CnYVV$&xi@O*(JBoJH`jrD zO{DNMK4FaJ>N(3_(lPY$9wkXV`uVJqck>N_HK4s>t$|vBAc-a zWMAxRuE)THVC_e`dPa%UqPC1!4ohY*SvsW(1T%DvkM22`A9=_lf=BS-#5e0 zq4OglVk!%PDPu?Wa2$Cntk2VRM+XqOXSn>je5*U5`50an3X}FD@zJTD9vt5kH+urU zQ^wynEyg}x`4Zy@f0Tu4nepKbg~{0&5tQw?cRS!TioB?v7nTkhKZ2pDAnHx_Hh7Rf zOF#pl^|7tsLZiUN&gR}xzHtE(q|AL9sT!wQ7f(Wn_T29=&k{@fzpYX3b2Y!}Vpc`8 z48V+BH$lj!vDo&7g5zap7ir$VYP6?aj3CjLEy!GMiCI(ddzz6b1)%bCCF^1wKm`GO zLGSlx{8ybvUHw)N!v@p{dRCZ!j-TlZ5lewVUG;){IzF~w>OaasFHzYtd4C}tyk={3 zIZ~#da;od;b&mS}OtY+%K5?9O1Xmzx+23^az>elYEVSM*H@dK^a==xmJ)BVT+;BNL z)@H~1RZal#WIyF9GVjPL*cBIDjgy`(YU=}|}rOn`x7 zlXc9qz3Ll;sf(=HCTw^-cJ%EC6b+DyzzY;JC;lfynhD!Sa8G>BYu7f!4czYE58^Oe z)WtgF#3OO7vVpOC$%-NT3HNlaV7WQ`w(SFG7M*e``T9eLT?R%Duo%KcC@O7*;|nlV zD9Y4N%KO(0DapC$PNeV2mKDxtI3I9DDkrXU@Y%9)O}!mq7rqi*RID;Hh^)-S*95Tq zK^b$$Stx1`H;bN79xO9~@Xk|>DGn@{u!c=Y8%;ga{iD5o@rfYR{ZAF&yJRd#HA3TT zh23zRAJ^~h-s!nu%^M}E*sHCgWOuo767`{>o%M8er7I`BY}{6poYo$mjDVocSBxDV zHIxU`6aArn;D+zfRX4i!9ghee@Dk?)zf*A~j>m;O5D@E1wQCYQl2F-&yVsSOT;CC> z5MpU7GuYNd%8vs$AUv^+|zvKT_G-(c57bF#6reu7mjmsA5BBPT;F3U zdT}cxcdKuDJcf^d#Ubc*!5Esq{fQx6XY=a4yK5M(S~=>$#xv^ER$Yc($u7iL`-Ax5 zp0_Ie6Rmj*M)1pGfj~A75zgpD=g;_9?ub_Yubl`d4$KedKHkb*be{uMViaup6jBxL X`J_{D{jd7v8fBq&*^*WpcR>FS7tk~G diff --git a/images/branding/logo.webp b/images/branding/logo.webp index 2132bca12a5caa39d4179ff615df0557dfad4e6e..b2d467e0985b500c7a1ef67dc24dca665b2fa992 100644 GIT binary patch literal 9582 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 kR`J8|$iFFMhGN3xV^>NI*@~ literal 5434 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 diff --git a/public/favicon.png b/public/favicon.png index 5bfb829e422d0e3caed8f6889a4b28408ee29c5f..9477cf287443721341a7129dfd0e8279f0ad1722 100644 GIT binary patch literal 4086 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 literal 5613 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 z z!+1u=e==_AXNC*ge5rjb{OK3`8azeizx4Y`Db&fJ&mxKC=9Cv&C~9ly&tU1+nH@oX zm8Q3FY&4-69`b>)Np*mm+gR|o0zgZmjayP)xw-G};4r3!{fpGFSGA5&{q(pE<2Uo{ zMw%Zwts&-K3STjy^u6F~r_JC7XS2n^l!Yf8>R4Lq!RK425hADUSJ3afuszQ)J1zx4nGs5WC|ocraPKH~A-~A<4w;LaNNEw$d^9r9wKj*d3uQ+b_(W!3A*@b$UPmpw=Nv;;kq(g{YR; z9`GPVe6Vhh9fo{Q_aj@Q+-0xANitxl7?Tps6x)u+^`2iP zI?mC?fkqY5iy3f-SCC()g$v8Xme$15Yqr{Dy_#pf^=7tIL;mdlO$J@-9imhX`sQA{ zZq~;!*v&C$NO0J7k1dH?#!kC*Y<^YQ=VSMe7vibH<)OK?v=2@I{BNfNy>zi#oET?L z%z;R<@n`()^3LNLN8My82yBI50nRwiK)FtL|>>)pR z$Uc?*c3LF6cB_H={Pb8FLQw%9hY_(;Nyx2#+PHt{rM=(ONfN_>g!ELPb<^Ic@wqCe zHynKaK4$%-mSjHLp^H31$gZZzQuIChx|>rEw7qFO4sUzd(UJz+t!7=EnW`%N%lFL2 z4QA=qZmy+ge>&KxZ6tkZWD8e|c$FRcFSCU$Dtcx+nbou%zFIa`5;Pu0ub1%8Sa$URW6 zUk}9GLp>-)dY32m^X?cOiEX_VVrUX>r}aojPc>hK!v99Jud&BXu1zm39+iDMm>{;T zhQ5h-+qSyd@??&f5FomI9UoyJz8u#uNAO5@x=5U|v3~2G8*yVcn?bAjSLMl8{CS>C z$I=f0P1L_msc=)5aBobwk2W^chf077Tt!t8uBwQDJHX-ERHz~3|KH+a-