From f39d34b7696760f14cc46f89d144c6bad8191cd1 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Jul 2024 11:49:47 +0200 Subject: [PATCH] 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)", + }, + }, + }; +}