Social
The social module connects accounts on Mastodon, LinkedIn and X, and publishes posts to them: at once, at a set time, or in the next free slot of a weekly schedule. It is the Go module smeldr.dev/social, and the release binary includes it.
Turn it on
In the release binary, set ENABLE_SOCIAL. Each platform's app settings are then kept in the database: an admin token sets them once with the MCP tool create_platform_config. (MASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET and MASTODON_INSTANCE_URL are an older way to set Mastodon's, still read when the database has none.)
In your own Go program:
import (
"smeldr.dev/mcp"
"smeldr.dev/social"
)
srv := social.New(db, social.Config{Secret: []byte(secret)})
srv.Register(app)
defer srv.Stop()
mcpSrv := mcp.New(app,
mcp.WithModule(srv.PostModule()),
mcp.WithModule(srv.CredentialModule()),
mcp.WithModule(srv.ConfigModule()),
mcp.WithModule(srv.ScheduleModule()),
)
Stop lets the scheduler finish what it is publishing.
Connect an account
- Set the platform's app settings, from the app you registered with the platform:
json {"platform": "mastodon", "client_id": "...", "client_secret": "...", "redirect_url": "https://smeldr.example.com/oauth/mastodon/callback", "instance_url": "https://mastodon.social"}
create_platform_config needs platform, client_id, client_secret and redirect_url. Mastodon also needs instance_url. scope and success_url (where the browser goes after connecting) are optional.
- Call
create_social_credentialwith the platform (andinstance_urlfor Mastodon). It answers with aredirect_urlon the platform.
- Open that URL in a browser and allow the app. The platform sends the browser back to
/oauth/<platform>/callback, and the account is saved as a credential. The link works once: using it again is refused withinvalid or expired OAuth state.
| Platform | Scope it asks for | Notes |
|---|---|---|
mastodon | write:statuses write:media | any instance, named by instance_url |
linkedin | openid profile w_member_social | |
x | tweet.read users.read tweet.write offline.access media.write | with PKCE |
Tokens are stored encrypted with a key derived from SECRET. list_social_credentials shows the connected accounts.
Posts
create_scheduled_post needs credential_id and body. Its other fields:
platform:mastodon(the default),linkedinorx;scheduled_at: an RFC 3339 time, which makes the postscheduled;status:queuedputs it in the credential's slot queue (see below);media_urlandalt_text: an image to attach, by its HTTPS URL.
Without scheduled_at or status, a post starts as a draft. publish_scheduled_post publishes a post at once:
{"slug": "01a1afda-f9c7-7ded-82fb-c559291f969d", "status": "published"}
The scheduler publishes a scheduled post when its time comes, checking at least once a minute. A post that fails is tried again after 30 seconds, 2 minutes, 10 minutes and an hour, and is then failed.
How long a post may be depends on the platform. X allows 280 characters, and a longer post fails. LinkedIn's text is cut at 3,000 characters. Mastodon's limit is the instance's own.
The slot queue
Instead of a time, a post can wait for the next slot in its credential's weekly schedule. Create the schedule with create_publication_schedule, with the slots as a JSON string:
{"credential_id": "01a1afda-f985-7c6e-be11-3680820af4ee",
"slots": "[{\"weekday\":1,\"time\":\"09:00\",\"timezone\":\"Europe/Copenhagen\"}]"}
weekdayis 0 (Sunday) to 6 (Saturday),timeisHH:MMon a 24-hour clock, andtimezoneis an IANA name.- A credential has at most one schedule. A second is refused; change the first with
update_publication_schedule. - A schedule is
activeorpaused. A paused schedule keeps its queue.
When a slot comes, the oldest queued post of that credential is published. If the server was down when slots passed, it catches up with one post per missed slot, and at most as many as the schedule has slots.
REST
GET /social/posts lists the posts, with a bearer token. ?status=queued filters by state: draft, scheduled, queued, published, failed or archived.
A credential that is still used by posts cannot be deleted:
Conflict: credential 01a1afda-f985-7c6e-be11-3680820af4ee is used by 4 post(s); delete them or move them to another credential first
MCP tools
| Tools | For |
|---|---|
create_platform_config, list_platform_configs, get_platform_config | a platform's app settings (admin). Calling create_platform_config again replaces a platform's settings. The client secret is never returned: a listing shows client_secret_set instead |
create_social_credential, list_social_credentials, get_social_credential, delete_social_credential | connected accounts |
create_scheduled_post, list_scheduled_posts, get_scheduled_post, update_scheduled_post, publish_scheduled_post, schedule_scheduled_post, archive_scheduled_post, delete_scheduled_post | posts |
create_publication_schedule, list_publication_schedules, get_publication_schedule, update_publication_schedule, delete_publication_schedule | slot schedules |
smeldr-cli social covers the same ground from a shell; see smeldr-cli.
Sending events to an agent (Go)
In a Go program, AddRoutes sends a JSON POST to an HTTPS URL when a content item is published, scheduled, archived or deleted:
srv.AddRoutes(app,
social.OnPublish("Post", "https://agent.example.com/hook"),
)
Each request carries X-Smeldr-Signature: sha256= and the hex HMAC-SHA256 of the body, keyed with the app's secret. The same value is also sent as X-Forge-Signature, which is deprecated and will be removed: verify X-Smeldr-Signature. A request that fails is tried again after 30 seconds, 2 minutes, 10 minutes and an hour. A 4xx answer other than 429 is not retried, and a 429 waits for its Retry-After.