Documentation
+{{ .Title }}
+ {{ with .Description }}{{ . }}
{{ end }} + {{ .Content }} + {{ partial "pager.html" . }} +diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..eab5516 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,3 @@ +/public/ +/resources/ +/.hugo_build.lock diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..db5f7b7 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,19 @@ +# shitpost docs UI + +This folder contains a minimal Hugo documentation site for `shitpost`. + +## Local development + +```sh +hugo server --source ui +``` + +## Build + +```sh +hugo --source ui --gc --minify +``` + +## Netlify + +Set the Netlify base directory to `ui`. Netlify will read `ui/netlify.toml`, run Hugo, and publish `ui/public`. diff --git a/ui/content/_index.md b/ui/content/_index.md new file mode 100644 index 0000000..5bbdb46 --- /dev/null +++ b/ui/content/_index.md @@ -0,0 +1,4 @@ +--- +title: "shitpost" +description: "Post once to Telegram, publish everywhere." +--- diff --git a/ui/content/getting-started/_index.md b/ui/content/getting-started/_index.md new file mode 100644 index 0000000..d77f8cc --- /dev/null +++ b/ui/content/getting-started/_index.md @@ -0,0 +1,39 @@ +--- +title: "Getting started" +description: "Run a private Telegram bot that crossposts text and media to your social accounts." +weight: 10 +--- + +`shitpost` is a small Go service that listens to your Telegram bot, downloads incoming media when needed, and calls the `crosspost` CLI with the platform flags you choose. + +The happy path is: + +1. Create a Telegram bot with BotFather. +2. Create a `.env` file from `.env.example`. +3. Add your Telegram token and the platform credentials you need. +4. Start in dry-run mode. +5. Send a message to your bot and inspect the command preview. +6. Disable dry-run when the output looks right. + +```sh +curl -o .env https://raw.githubusercontent.com/bupd/shitpost/main/.env.example +docker run -d --name shitpost \ + --env-file .env \ + -v ./downloads:/app/downloads \ + registry.goharbor.io/bupd/shitpost:latest +``` + +## What it does + +- Accepts Telegram text messages and posts them through `crosspost`. +- Accepts Telegram photos, downloads the largest image, and passes it to `crosspost` with optional alt text. +- Accepts video and document uploads, stores them locally, and posts caption text when the installed `crosspost` media path cannot attach them. +- Replies back in Telegram with the `crosspost` logs or the dry-run command preview. +- Can restrict usage to specific Telegram usernames or numeric user IDs. + +## What to read next + +- [Installation](/getting-started/installation/) for Docker, Compose, and local development. +- [Authentication](/getting-started/authentication/) for Telegram and platform tokens. +- [Configuration](/getting-started/configuration/) for every environment variable. +- [Architecture](/getting-started/architecture/) for the full request flow. diff --git a/ui/content/getting-started/architecture.md b/ui/content/getting-started/architecture.md new file mode 100644 index 0000000..ce0238a --- /dev/null +++ b/ui/content/getting-started/architecture.md @@ -0,0 +1,61 @@ +--- +title: "Architecture" +description: "How Telegram messages move through shitpost, crosspost, and target platforms." +weight: 50 +--- + +`shitpost` is intentionally thin. It owns the Telegram interface, local media download, environment normalization, and Telegram replies. Publishing is delegated to `crosspost`. + +```text +Telegram user + -> Telegram Bot API + -> shitpost Go process + -> crosspost CLI + -> Bluesky / Mastodon / X / other targets +``` + +## Runtime pieces + +| Piece | Responsibility | +| --- | --- | +| Telegram Bot API | Delivers messages, photos, videos, and documents to the bot. | +| `main.go` | Polls updates, authorizes users, parses messages, downloads files, and starts `crosspost`. | +| `downloads/` | Local working directory for Telegram media files. | +| `crosspost` CLI | Publishes to the selected social platforms. | +| `.env` | Holds Telegram and platform credentials. | +| Docker image | Bundles the Go bot, Bun runtime, and built `crosspost` CLI. | + +## Message flow + +1. The bot starts and reads `BOT_TOKEN`, `AUTHORIZED_TELEGRAM_USERS`, `CROSSPOST_FLAGS`, and `SHITPOST_DRY_RUN`. +2. The Telegram SDK opens a long-polling update channel. +3. Non-message updates are ignored. +4. If `AUTHORIZED_TELEGRAM_USERS` is set, the sender username or numeric ID must match. +5. Text messages call `crosspost` directly with the message body. +6. Photos are downloaded from Telegram, saved to `downloads/`, and passed to `crosspost` with `--image`. +7. Captions ending with `alt:` are split into clean caption and image alt text. +8. Videos and non-image documents are downloaded, but only caption text is posted by the current CLI path. +9. The bot sends the captured `crosspost` output back to the Telegram chat. + +## Docker build flow + +The Dockerfile has two stages: + +1. A Go builder compiles the static `bot` binary. +2. A Bun runtime clones `crosspost`, installs dependencies, builds it, writes a `/usr/local/bin/crosspost` wrapper, and copies the Go binary. + +The final container starts `./bot` from `/app` and stores downloaded media in `/app/downloads`. + +## CI and releases + +Pull requests and pushes run formatting, vet, build, and tests. Pushes to `main` also build multi-arch images for `linux/amd64` and `linux/arm64`, sign them, publish to GHCR, and copy `latest` to Harbor. + +Tags beginning with `v` run GoReleaser, publish release archives, build semver image tags, copy them to Harbor, and sign the images. + +## Security model + +- The service is self-hosted; messages and downloaded media stay on your machine or server. +- No web UI is exposed by this project. +- The Telegram bot should be treated as a private command surface. +- `AUTHORIZED_TELEGRAM_USERS` should be set for any real account. +- `.env` should never be committed or shared. diff --git a/ui/content/getting-started/authentication.md b/ui/content/getting-started/authentication.md new file mode 100644 index 0000000..1d1d933 --- /dev/null +++ b/ui/content/getting-started/authentication.md @@ -0,0 +1,93 @@ +--- +title: "Authentication" +description: "Create the Telegram bot and collect the platform credentials that crosspost needs." +weight: 30 +--- + +## Telegram bot token + +1. Open [@BotFather](https://t.me/BotFather) in Telegram. +2. Send `/newbot`. +3. Pick a display name and bot username. +4. Copy the token BotFather returns. +5. Put it in `.env` as `BOT_TOKEN`. + +```dotenv +BOT_TOKEN=123456789:telegram-secret +``` + +Treat the bot token like a password. Anyone with it can control the bot API for that bot. + +## Private bot access + +Set `AUTHORIZED_TELEGRAM_USERS` so only trusted Telegram accounts can trigger posts. + +```dotenv +AUTHORIZED_TELEGRAM_USERS=alice,123456789 +``` + +Accepted values are Telegram usernames without `@`, usernames with `@`, or numeric Telegram user IDs. Separate multiple users with commas. + +Leaving this variable empty means anyone who can message the bot can ask it to post. That is only safe for a throwaway test bot. + +## Bluesky credentials + +Use an app password instead of your account password. + +1. Open Bluesky settings. +2. Go to app passwords. +3. Create a new app password for `shitpost`. +4. Set your handle or email as `BLUESKY_IDENTIFIER`. +5. Set the app password as `BLUESKY_PASSWORD`. + +```dotenv +BLUESKY_HOST=bsky.social +BLUESKY_IDENTIFIER=you.bsky.social +BLUESKY_PASSWORD=xxxx-xxxx-xxxx-xxxx +``` + +## Mastodon credentials + +Create an application from your Mastodon instance preferences. + +1. Open your Mastodon instance in a browser. +2. Go to Preferences, then Development. +3. Create a new application with write scopes. +4. Copy the access token. +5. Set the instance URL and token in `.env`. + +```dotenv +MASTODON_HOST=https://mastodon.social +MASTODON_ACCESS_TOKEN=secret-token +``` + +Some `crosspost` flows may also use `MASTODON_CLIENT_KEY` and `MASTODON_CLIENT_SECRET`; keep them available if your target command needs them. + +## Twitter / X credentials + +`shitpost` supports the `crosspost` Twitter strategies and normalizes several legacy aliases before invoking `crosspost`. + +For the emusks-backed strategy, provide the X `auth_token` cookie value: + +```dotenv +AUTH_TOKEN=your-x-auth-token-cookie +``` + +You can also set the explicit variable: + +```dotenv +TWITTER_AUTH_TOKEN=your-x-auth-token-cookie +``` + +If both are set, `TWITTER_AUTH_TOKEN` wins. + +For official API fallback paths, use OAuth 1.0a credentials: + +```dotenv +TWITTER_API_CONSUMER_KEY= +TWITTER_API_CONSUMER_SECRET= +TWITTER_ACCESS_TOKEN_KEY= +TWITTER_ACCESS_TOKEN_SECRET= +``` + +To find the X `auth_token` cookie, sign in to X in a browser you control, open developer tools, inspect cookies for `x.com`, and copy the value named `auth_token`. Do not paste the full cookie header; only the token value belongs in `.env`. diff --git a/ui/content/getting-started/configuration.md b/ui/content/getting-started/configuration.md new file mode 100644 index 0000000..7360c24 --- /dev/null +++ b/ui/content/getting-started/configuration.md @@ -0,0 +1,87 @@ +--- +title: "Configuration" +description: "Environment variables, target flags, aliases, and safe validation." +weight: 40 +--- + +## Minimal `.env` + +Start with the example file: + +```sh +cp .env.example .env +``` + +The smallest useful dry-run config is: + +```dotenv +BOT_TOKEN=123456789:telegram-secret +AUTHORIZED_TELEGRAM_USERS=your_username +CROSSPOST_FLAGS=-bmt +SHITPOST_DRY_RUN=1 +``` + +`CROSSPOST_FLAGS=-bmt` tells `crosspost` to post to Bluesky, Mastodon, and Twitter/X. Change it to match the destinations you actually configured. + +## Core variables + +| Variable | Required | Purpose | +| --- | --- | --- | +| `BOT_TOKEN` | Yes | Telegram bot token from BotFather. | +| `AUTHORIZED_TELEGRAM_USERS` | Recommended | Comma-separated usernames or numeric IDs allowed to use the bot. Empty means public. | +| `CROSSPOST_FLAGS` | No | Flags passed directly to `crosspost`; defaults to `-bmt`. | +| `SHITPOST_DRY_RUN` | No | Set to `1`, `true`, or `yes` to preview commands without posting. | +| `CROSSPOST_REPO` | No | Source repo used when building the Docker image. | +| `CROSSPOST_REF` | No | Branch, tag, or ref used when building the Docker image. | + +## Platform variables + +| Variable | Platform | Purpose | +| --- | --- | --- | +| `BLUESKY_HOST` | Bluesky | Usually `bsky.social`. | +| `BLUESKY_IDENTIFIER` | Bluesky | Handle or email. | +| `BLUESKY_PASSWORD` | Bluesky | App password. | +| `MASTODON_HOST` | Mastodon | Instance URL, such as `https://mastodon.social`. | +| `MASTODON_ACCESS_TOKEN` | Mastodon | Access token with posting permission. | +| `MASTODON_CLIENT_KEY` | Mastodon | Optional client key for crosspost flows that need it. | +| `MASTODON_CLIENT_SECRET` | Mastodon | Optional client secret for crosspost flows that need it. | +| `AUTH_TOKEN` | X | Alias for the X `auth_token` cookie. | +| `TWITTER_AUTH_TOKEN` | X | Explicit X `auth_token`; overrides `AUTH_TOKEN`. | +| `TWITTER_AUTH_CLIENT` | X | Optional client identity understood by the emusks-backed strategy. | +| `TWITTER_GRAPHQL_ENDPOINT` | X | Optional endpoint profile understood by the emusks-backed strategy. | +| `TWITTER_PROXY` | X | Optional proxy for X requests. | +| `TWITTER_API_CONSUMER_KEY` | X | Official API consumer key fallback. | +| `TWITTER_API_CONSUMER_SECRET` | X | Official API consumer secret fallback. | +| `TWITTER_ACCESS_TOKEN_KEY` | X | Official API access token fallback. | +| `TWITTER_ACCESS_TOKEN_SECRET` | X | Official API access token secret fallback. | + +## Alias normalization + +Before `shitpost` starts `crosspost`, it copies legacy aliases into the variable names `crosspost` expects when the target variable is empty. + +| Target | Accepted aliases | +| --- | --- | +| `TWITTER_AUTH_TOKEN` | `AUTH_TOKEN` | +| `TWITTER_API_CONSUMER_KEY` | `consumer_key`, `TWITTER_CONSUMER_KEY` | +| `TWITTER_API_CONSUMER_SECRET` | `consumer_key_secret`, `TWITTER_CONSUMER_SECRET` | +| `TWITTER_ACCESS_TOKEN_KEY` | `access_token`, `access_token_key`, `TWITTER_ACCESS_TOKEN` | +| `TWITTER_ACCESS_TOKEN_SECRET` | `access_token_secret`, `TWITTER_ACCESS_SECRET` | + +## Validate without leaking secrets + +Run the doctor task when posts fail or before deploying a new `.env`: + +```sh +task doctor +``` + +The doctor script prints whether each key is present and the value length. It does not print secret values. + +## Recommended first run + +```dotenv +SHITPOST_DRY_RUN=1 +CROSSPOST_FLAGS=-bmt +``` + +Send one text message and one image with a caption. If the Telegram replies show the expected command, set `SHITPOST_DRY_RUN=0` and restart the service. diff --git a/ui/content/getting-started/deployment.md b/ui/content/getting-started/deployment.md new file mode 100644 index 0000000..244753f --- /dev/null +++ b/ui/content/getting-started/deployment.md @@ -0,0 +1,77 @@ +--- +title: "Deployment" +description: "Run the bot on a server and deploy these docs on Netlify." +weight: 70 +--- + +## Bot deployment + +For a server, prefer the published container image and a persistent `downloads` directory. + +```sh +mkdir -p /opt/shitpost/downloads +cd /opt/shitpost +curl -o .env https://raw.githubusercontent.com/bupd/shitpost/main/.env.example +``` + +Edit `/opt/shitpost/.env`, then run: + +```sh +docker run -d --name shitpost \ + --restart unless-stopped \ + --env-file /opt/shitpost/.env \ + -v /opt/shitpost/downloads:/app/downloads \ + registry.goharbor.io/bupd/shitpost:latest +``` + +Check logs: + +```sh +docker logs -f shitpost +``` + +## Docker Compose deployment + +The repo includes `docker-compose.yml`: + +```sh +cp .env.example .env +docker compose up --build -d +docker compose logs -f shitpost-bot +``` + +The Compose service is named `shitpost-bot` and the container is named `shitpost-engine`. + +## Image registries + +Published images are available from: + +```sh +docker pull registry.goharbor.io/bupd/shitpost:latest +docker pull ghcr.io/bupd/shitpost:latest +``` + +For production, use a versioned tag when one is available. + +## Docs deployment on Netlify + +This `ui` folder is a Hugo site. In Netlify: + +1. Connect the Git repository. +2. Set the base directory to `ui`. +3. Keep the build command as `hugo --gc --minify`. +4. Keep the publish directory as `public`. + +`ui/netlify.toml` already contains those settings. + +## Local docs preview + +```sh +hugo server --source ui +``` + +Build the static site: + +```sh +hugo --source ui --gc --minify +``` diff --git a/ui/content/getting-started/installation.md b/ui/content/getting-started/installation.md new file mode 100644 index 0000000..512c55d --- /dev/null +++ b/ui/content/getting-started/installation.md @@ -0,0 +1,87 @@ +--- +title: "Installation" +description: "Install and run shitpost with Docker, Docker Compose, or Go." +weight: 20 +--- + +## Requirements + +- Docker or Podman for the recommended setup. +- Go 1.25+ if you run the bot locally. +- A Telegram bot token from [BotFather](https://t.me/BotFather). +- Credentials for each social platform you want `crosspost` to publish to. + +## Option 1: run the image + +Use the published image when you want the simplest production path. + +```sh +mkdir -p downloads +curl -o .env https://raw.githubusercontent.com/bupd/shitpost/main/.env.example +``` + +Edit `.env`, then run: + +```sh +docker run -d --name shitpost \ + --env-file .env \ + -v ./downloads:/app/downloads \ + registry.goharbor.io/bupd/shitpost:latest +``` + +Follow logs: + +```sh +docker logs -f shitpost +``` + +Use a versioned tag for production once releases are available. `latest` tracks the newest build from `main`. + +## Option 2: Docker Compose + +Clone the repo and run the Compose workflow: + +```sh +git clone https://github.com/bupd/shitpost.git +cd shitpost +cp .env.example .env +docker compose up --build +``` + +The service mounts `./downloads` into `/app/downloads` so Telegram media survives container restarts. + +## Option 3: Taskfile workflow + +If you have [Task](https://taskfile.dev/) installed, the repo exposes common workflows: + +```sh +task setup +task up:dry-run +task logs +task down +task doctor +task validate +``` + +Start with `task up:dry-run`. Your Telegram messages will not be posted; the bot replies with the exact `crosspost` command it would run. + +## Option 4: local Go process + +Local mode is useful while changing bot behavior. + +```sh +cp .env.example .env +task dev:dry-run +``` + +The local process expects `crosspost` to be available on `PATH` if dry-run is disabled. The Docker image already installs and wraps `crosspost` for you. + +## Confirm it works + +The bot is ready when logs include: + +```text +Authorized as @your_bot_username +``` + +Send a Telegram message to the bot. In dry-run mode, the reply should start with `DRY RUN: would run`. diff --git a/ui/content/getting-started/troubleshooting.md b/ui/content/getting-started/troubleshooting.md new file mode 100644 index 0000000..f72c29d --- /dev/null +++ b/ui/content/getting-started/troubleshooting.md @@ -0,0 +1,80 @@ +--- +title: "Troubleshooting" +description: "Common startup, auth, media, and platform posting failures." +weight: 80 +--- + +## Bot fails on startup + +Check `BOT_TOKEN` first. If it is missing, the process exits immediately with: + +```text +BOT_TOKEN environment variable is required +``` + +If the token is wrong, Telegram bot creation fails. Regenerate or copy the token again from BotFather. + +## Bot starts but ignores you + +If `AUTHORIZED_TELEGRAM_USERS` is set, your Telegram username or numeric user ID must be in the comma-separated list. + +```dotenv +AUTHORIZED_TELEGRAM_USERS=your_username,123456789 +``` + +Remove the leading `@` or keep it; `shitpost` normalizes both forms. + +## Posts do not appear + +Run the doctor script: + +```sh +task doctor +``` + +Confirm the target platform variables are present. Then run dry-run mode and inspect the command preview: + +```sh +task up:dry-run +``` + +If the command preview is wrong, fix `CROSSPOST_FLAGS`. If the command preview is right but live posting fails, inspect the `crosspost` stderr returned in Telegram. + +## X returns 401 + +Confirm you are using the correct X credential path. + +For the emusks-backed strategy, set only the cookie value: + +```dotenv +AUTH_TOKEN=your-auth-token-cookie-value +``` + +For official API fallback, verify `TWITTER_ACCESS_TOKEN_KEY` is an OAuth 1.0a access token. The doctor script warns if the value does not look like a typical OAuth token. + +## Media does not upload + +Check that `downloads/` exists and is writable by the container. + +```sh +mkdir -p downloads +docker compose up --build +``` + +Images are attached through `crosspost`. Videos and non-image documents currently post caption text only. + +## Logs are too long for Telegram + +Telegram has a message length limit. `shitpost` chunks long replies so you still receive the command result and logs. + +## Reset safely + +Stop and remove the container without deleting your `.env` or downloaded media: + +```sh +docker rm -f shitpost +docker run -d --name shitpost \ + --env-file .env \ + -v ./downloads:/app/downloads \ + registry.goharbor.io/bupd/shitpost:latest +``` diff --git a/ui/content/getting-started/usage.md b/ui/content/getting-started/usage.md new file mode 100644 index 0000000..e296528 --- /dev/null +++ b/ui/content/getting-started/usage.md @@ -0,0 +1,64 @@ +--- +title: "Usage" +description: "Send text, images, captions, and alt text from Telegram." +weight: 60 +--- + +## Text posts + +Send a normal text message to your Telegram bot. `shitpost` passes that text as the final argument to `crosspost`. + +```text +shipping a small self-hosted crossposter today +``` + +In dry-run mode, the bot replies with a command preview. In live mode, it replies with `crosspost` stdout and stderr. + +## Image posts + +Send a photo with an optional caption. Telegram provides multiple photo sizes; `shitpost` picks the largest one, saves it under `downloads/`, and passes it with `--image`. + +```text +caption: a tiny bot doing useful work +``` + +## Alt text + +Add alt text by ending the caption with a final line that starts with `alt:`. + +```text +new deploy view from the homelab +alt: A terminal window showing a successful Docker deployment +``` + +The posted caption becomes `new deploy view from the homelab`. The alt text is passed separately to `crosspost`. + +## Videos and documents + +Telegram videos and documents are downloaded so the bot can acknowledge and inspect them. The current installed `crosspost` CLI path only attaches images, so video and non-image document messages post caption text only. + +If there is no caption, the bot replies with a warning instead of posting an empty update. + +## Dry-run mode + +Dry-run mode is the safest way to test credentials, target flags, captions, and alt text. + +```dotenv +SHITPOST_DRY_RUN=1 +``` + +Restart the service after changing `.env`. The startup logs should include: + +```text +Dry-run mode enabled. Messages will not be posted. +``` + +## Target selection + +`CROSSPOST_FLAGS` is passed directly to `crosspost`. + +```dotenv +CROSSPOST_FLAGS=-bmt +``` + +Use the flags supported by the `crosspost` version you build into the image. The default project configuration targets Bluesky, Mastodon, and Twitter/X. diff --git a/ui/hugo.toml b/ui/hugo.toml new file mode 100644 index 0000000..d9e1ff3 --- /dev/null +++ b/ui/hugo.toml @@ -0,0 +1,70 @@ +baseURL = "https://shitpost.dev/" +languageCode = "en-us" +title = "shitpost" +disableKinds = ["taxonomy", "term"] + +[params] +description = "Telegram-powered social media crossposting for self-hosters." +repo = "https://github.com/bupd/shitpost" +image = "registry.goharbor.io/bupd/shitpost:latest" + +[markup] + [markup.highlight] + noClasses = false + [markup.goldmark.renderer] + unsafe = true + +[[menus.main]] +name = "Home" +url = "/" +weight = 10 + +[[menus.main]] +name = "Getting started" +url = "/getting-started/" +weight = 20 + +[[menus.main]] +name = "GitHub" +url = "https://github.com/bupd/shitpost" +weight = 30 + +[[menus.docs]] +name = "Overview" +url = "/getting-started/" +weight = 10 + +[[menus.docs]] +name = "Installation" +url = "/getting-started/installation/" +weight = 20 + +[[menus.docs]] +name = "Authentication" +url = "/getting-started/authentication/" +weight = 30 + +[[menus.docs]] +name = "Configuration" +url = "/getting-started/configuration/" +weight = 40 + +[[menus.docs]] +name = "Architecture" +url = "/getting-started/architecture/" +weight = 50 + +[[menus.docs]] +name = "Usage" +url = "/getting-started/usage/" +weight = 60 + +[[menus.docs]] +name = "Deployment" +url = "/getting-started/deployment/" +weight = 70 + +[[menus.docs]] +name = "Troubleshooting" +url = "/getting-started/troubleshooting/" +weight = 80 diff --git a/ui/layouts/_default/baseof.html b/ui/layouts/_default/baseof.html new file mode 100644 index 0000000..81f5597 --- /dev/null +++ b/ui/layouts/_default/baseof.html @@ -0,0 +1,18 @@ + + +
+ + +Documentation
+{{ . }}
{{ end }} + {{ .Content }} + {{ partial "pager.html" . }} +Documentation
+{{ . }}
{{ end }} + {{ .Content }} + {{ partial "pager.html" . }} +Telegram-powered crossposting
+shitpost is a tiny self-hosted Go bot that turns your private Telegram chat into a social posting command center.
docker run -d --name shitpost \
+ --env-file .env \
+ -v ./downloads:/app/downloads \
+ {{ site.Params.image }}
+ Send text, images, captions, and alt text to one bot instead of opening every social app.
+Your tokens, media, and logs stay on your server. No hosted dashboard is required.
+The Go bot handles Telegram. The `crosspost` CLI handles Bluesky, Mastodon, X, and more.
+Preview the exact command before anything is published. Fix flags and tokens safely.
+Architecture
+shitpost polls Telegram, checks authorization, downloads media, normalizes environment variables, and starts crosspost. That is the whole shape.