hyungyunlim shared this post ยท 4d ago
alexandru

alexandru/social-publish: Self-hosted service that helps me share to multiple social media platforms at the same time.

Social Publish

In implementing POSSE (publish on your own site, syndicate elsewhere), I need to publish to multiple social networks. Social Publish provides direct API integration for X (Twitter), Mastodon, Bluesky, and LinkedIn, plus an RSS feed for automation.

What it does

  • Publish the same post to multiple networks from one form
  • Upload images with alt-text, or let an LLM generate alt-text automatically
  • Provide an RSS feed for external automation tools like IFTTT

Supported networks

Table of Contents

Self-hosting

My docker-compose setup:

version: "3.8"

services:

...

social-publish:
container_name: social-publish
image: ghcr.io/alexandru/social-publish:latest
restart: always
healthcheck:
test: ["CMD-SHELL", "curl --head http://localhost:3000/ || exit 1"]
ports:

  • "3000:3000"
    env_file:
  • ./envs/social-publish.env
    networks:
  • external_network

Where ./envs/social-publish.env contains:

# Where the server is hosted โ€” needed for correctly generating an RSS feed
BASE_URL="https://your-hostname.com"

The server's Basic AUTH credentials

SERVER_AUTH_USERNAME="your-username"
SERVER_AUTH_PASSWORD="your-password"

Bluesky credentials

BSKY_HOST="https://bsky.social"
BSKY_USERNAME="your-username"
BSKY_PASSWORD="your-password"

Mastodon credentials

MASTODON_HOST="https://mastodon.social"
MASTODON_ACCESS_TOKEN="your-access-token"

Twitter OAuth1 key and secret (Consumer Keys in the Developer Portal)

TWITTER_OAUTH1_CONSUMER_KEY="Api Key"
TWITTER_OAUTH1_CONSUMER_SECRET="Api Secret Key"

LinkedIn OAuth2 credentials

LINKEDIN_CLIENT_ID="your-client-id"
LINKEDIN_CLIENT_SECRET="your-client-secret"

LLM for alt-text generation (optional)

Configure the API endpoint, key, and model for your LLM provider

For OpenAI:

LLM_API_URL="https://api.openai.com/v1/chat/completions"
LLM_API_KEY="your-openai-api-key"
LLM_MODEL="gpt-4o-mini"

For Mistral:

LLM_API_URL="https://api.mistral.ai/v1/chat/completions"

LLM_API_KEY="your-mistral-api-key"

LLM_MODEL="pixtral-12b-2409"

Used for authentication (https://jwt.io)

JWT_SECRET="random string"
```

Bluesky credentials

For Bluesky, you'll need an "app password".

Mastodon credentials

For Mastodon, you'll need an "access token". Here's how to get one:

  • Go to: https://mastodon.social/settings/applications
  • Create a "New Application"
  • Select write:statuses and write:media for permissions, and unselect everything else
  • Click on the newly created application
  • Copy " your access token "
  • Set the MASTODON_ACCESS_TOKEN environment variable to it

Twitter setup

For Twitter, we're working with Oauth1.

  • Go to: https://developer.twitter.com/en/portal/projects-and-apps
  • Create a project and app
  • In the " Keys and tokens " section of the app, generate " Consumer Keys " and copy the generated " App Key and Secret "
  • In the app's settings, go to " User authentication settings " and add as the " Callback URL ": https:///api/twitter/callback (replace `` with your domain, obviously)
  • Set the TWITTER_OAUTH1_CONSUMER_KEY and the TWITTER_OAUTH1_CONSUMER_SECRET environment variables
  • Once the server is running, go to https:///account and click on " Connect Twitter "

LinkedIn setup

For LinkedIn, we're working with OAuth2.

  • Go to: https://www.linkedin.com/developers/apps
  • Click " Create app " and fill in the required details
  • In the " Auth " tab, copy the " Client ID " and " Client Secret "
  • Add the following redirect URL: https:///api/linkedin/callback (replace `` with your actual domain)
  • In the " Products " tab, request access to: " Sign In with LinkedIn using OpenID Connect " (provides openid and profile scopes)
  • " Share on LinkedIn " (provides w_member_social scope) Set the LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET environment variables Once the server is running, go to https:///account and click on " Connect LinkedIn " Note: LinkedIn access tokens expire after 60 days. The system automatically refreshes tokens using the refresh token, which is valid for 1 year. You'll need to reconnect if the refresh token expires.

LLM setup (Optional)

The application can integrate with LLM providers to automatically generate alt-text descriptions for images. This feature is optional and supports any OpenAI-compatible API (including OpenAI, Mistral AI, and other providers).

Supported providers:

  1. Get an API key from your chosen provider
  2. Set the environment variables: - LLM_API_URL: The API endpoint URL (e.g., https://api.openai.com/v1/chat/completions) - LLM_API_KEY: Your API key - LLM_MODEL: Model name (e.g., gpt-4o-mini for OpenAI, pixtral-12b-2409 for Mistral)

CLI Commands

Social Publish includes several CLI commands for managing users and configuration. These commands are particularly useful when running the application in a Docker container.

Using the CLI in Docker

When running in Docker, you can use the ./cli wrapper script for cleaner output with minimal logging:

# Using docker exec with the cli wrapper (minimal logging)
docker exec -it social-publish /opt/app/cli create-user --username myuser --password mypassword

Or use java-exec directly with verbose flag for detailed logging

docker exec -it social-publish /opt/app/java-exec -jar /opt/app/app.jar create-user --username myuser --password mypassword --verbose
```

Available Commands

Create a new user:

./cli create-user --username --password
# Or with environment variables:
./cli create-user --db-path $DB_PATH --username --password

Change a user's password:

./cli change-password --username --new-password
# Prompts for password if not provided:
./cli change-password --username

Change a user's username:

./cli change-username --current-username --new-username
# Prompts for usernames if not provided:
./cli change-username

Generate a BCrypt password hash:

./cli gen-bcrypt-hash --password
# Or let it prompt you (hides input):
./cli gen-bcrypt-hash

Verbose Logging

By default, CLI commands use minimal logging (warnings and errors only). To see detailed logs, add the --verbose or -v flag:

./cli create-user --username myuser --password mypass --verbose

RSS feed

The RSS feed is exposed at /rss (e.g., http://localhost:3000/rss). Use it with automation tools like ifttt.com if you want additional workflows beyond the direct integrations.

Developing

This is a Kotlin multiplatform project with:

  • Backend: Ktor server with Arrow for functional programming
  • Frontend: Compose for Web (Kotlin/JS)
  • Build: Gradle with Kotlin DSL

Development Commands

To run the development environment with live reload:

make dev

This starts both the backend server (port 3000) and frontend dev server (port 3002) with hot reload enabled.

To run backend and frontend separately:

# Backend only
make dev-backend

Frontend only

make dev-frontend
```

You can navigate to http://localhost:3002 for the frontend, while the backend is available at http://localhost:3000.

Building

To build the project:

make build

To run tests:

make test

To check and fix code formatting:

make lint # Check formatting
make format # Auto-format code

Docker Images

To build and test the Docker images locally:

# Build and run JVM image
make docker-run-jvm

To run tests in a Docker environment that matches production:

# Run all tests in Docker
make test-docker

Run specific ImageMagick tests in Docker

make test-imagemagick-docker
```

See the Makefile for all available commands.

License

This project is licensed under the GNU Affero General Public License v3 (AGPL-3.0). See LICENSE.txt for details.