Lemmy (lemmy)
Lemmy is a federated link-aggregator and discussion platform (a Reddit-style network of independent instances). Rustiz connects to one instance with a username and password, logs in again for every publish call, and can create a post in several communities at once. It also supports comments on the resulting posts.
- Rust source:
crates/rustiz-providers/src/lemmy.rs - Upstream port:
lemmy.provider.ts - Editor: Normal (plain text after HTML sanitization)
- Connection mode:
customFields
Overview
| Property | Value |
|---|---|
| Identifier | lemmy |
| Display name | Lemmy |
| Connection mode | customFields (instance URL, identifier, password) |
| Editor | normal |
| Channel kind | social |
| Comments / threads | Yes. supports_comments = true; each comma-separated post id gets a top-level comment |
| Pending / async publishing | No. supports_pending = false; returns PostStatus::Success |
| Analytics | No. supports_analytics = false |
| Post analytics | No. supports_post_analytics = false |
| Mentions | No. supports_mentions = false |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 3 |
Character limit (max_length) | 10000 characters for the post body |
| API | Lemmy HTTP API v3 (/api/v3/...) |
Server Environment Variables
required_env() returns no variables:
#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
&[]
}
}
required_env_alternatives() is not overridden. The instance URL and account
come from the channel, so rustiz providers --status reports lemmy as
configured without server configuration.
| Variable | Required | Default | Purpose |
|---|---|---|---|
JWT_SECRET | No, but should be set | "" | Encrypts/decrypts the channel’s custom_instance_details (service, identifier, password). It must be the same value at connect time and on every later call. If it changes, decryption fails with Lemmy credentials are missing/invalid, please reconnect the channel. If it is unset consistently, an empty key is used (functional but insecure). |
ALLOW_PRIVATE_NETWORK | No | false | Global SSRF switch. A local test instance on localhost or a private IP is blocked unless this is true (or 1). |
There is no LEMMY_BASE_URL override. The service field in the
encrypted credentials is the base URL for every request.
Why JWT_SECRET matters
Connecting stores the credentials encrypted with fixed_encryption in
crates/rustiz-server/src/oauth.rs. get_jwt_and_service() decrypts them on
every post, comment and community search. The connect step itself succeeds
without decrypting, so a secret that changed after connect can look like a
successfully connected channel that fails on its first publish. Set a strong
secret before connecting and never rotate it without reconnecting every Lemmy
channel.
Step-by-Step Developer App Setup
Lemmy has no developer app, no OAuth client and no API-key portal. You use a normal account on an instance.
-
Choose the Lemmy instance (for example
https://lemmy.world) and create an account there, or use an existing account. -
Verify the account’s email if the instance requires it. Many instances hold new accounts in a registration-approval queue; wait until the account can log in through the web UI.
-
Make sure the account can post in the target communities. A community may be restricted, the account may be banned from an instance, or the instance may block the URL in a link post. Rustiz maps these errors to specific messages.
-
Do not enable two-factor authentication on the account. Rustiz’s login body sends only
username_or_emailandpassword; it has nototp_2fa_tokenfield, so a 2FA-enabled account cannot authenticate. -
Use the username as the
identifier, not the email address. The login endpoint accepts either, but the follow-up user lookup callsGET /api/v3/user?username={identifier}; an email in that query does not resolve to a user and the connect fails withInvalid credentials. -
Verify the login manually:
curl -s -X POST https://lemmy.world/api/v3/user/login \ -H 'Content-Type: application/json' \ -d '{"username_or_email":"alice","password":"..."}'A successful response contains
{"jwt":"..."}. Then:curl -s 'https://lemmy.world/api/v3/user?username=alice' \ -H "Authorization: Bearer $JWT"Rustiz reads
person_view.person.id,display_name/nameandavatarfrom this response.
Products and permissions
- No products or APIs to enable.
- No OAuth scopes. The account’s normal instance permissions apply.
- Posting to a community requires that the community exists, is not deleted or locked, and does not block the account or the post URL.
- Comments require the post to still exist and not be locked.
Sandbox / testing mode
There is no hosted sandbox. Options:
- Run a local Lemmy instance and set
ALLOW_PRIVATE_NETWORK=true. - Use a low-traffic test community on a real instance.
- Test with a text-only post first; a link post or image cover exercises more failure modes (blocked URL, missing thumbnail, federation delays).
Lemmy is federated: a post can succeed on the local instance but take time to appear on other instances. Rustiz returns as soon as the local API responds.
Production app review
There is no app review. Access is immediate and controlled by the instance’s registration policy and community moderation. The same credentials work in production.
API version compatibility
Rustiz hard-codes the v3 endpoints:
POST /api/v3/user/loginGET /api/v3/user?username=...POST /api/v3/postPOST /api/v3/commentGET /api/v3/search?type_=Communities&sort=Active&q=...
Lemmy 1.0 introduced API v4 and renamed account endpoints (for example
/api/v3/user/login → /api/v4/account/auth/login). Lemmy’s 1.0
compatibility layer keeps v3 available for general browsing and commenting,
so these calls should continue to work on 1.0 instances, but this provider
does not use any v4-only feature and has not been updated for v4. If an
instance disables v3, the channel cannot connect or publish.
Connecting a Channel (CLI & MCP)
The credential payload has three keys:
| Key | Type | Validation regex | Notes |
|---|---|---|---|
service | text | /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/ | Instance base URL. Default value is https://lemmy.world. Must include the scheme and a host with a dot; a bare lemmy.world fails. |
identifier | text | /^.{3,}$/ | Username (preferred) or email. At least 3 characters. |
password | password | /^.{3,}$/ | Account password. At least 3 characters in the form; the instance may require more. |
CLI
{
"service": "https://lemmy.world",
"identifier": "alice",
"password": "correct horse battery staple"
}
rustiz channels connect lemmy --fields lemmy.json
printf '%s' '{"service":"https://lemmy.world","identifier":"alice","password":"..."}' \
| rustiz channels connect lemmy --fields -
--external-url is not the right flag for Lemmy. It invokes the
externalUrl() OAuth-app-registration path, which Lemmy does not implement.
Use --fields so the provider receives service.
Immediate result:
{ "connected": true, "pending": false, "name": "Alice" }
MCP
{
"provider": "lemmy",
"customFields": {
"service": "https://lemmy.world",
"identifier": "alice",
"password": "correct horse battery staple"
}
}
No follow-up step. Call integrationList for the channel id.
Provider functions (triggerTool)
| Function | Parameters | Returns | Notes |
|---|---|---|---|
subreddits | {"word": "<keyword>"} | [{"title": "<community title>", "name": "<community title>", "id": <numeric community id>}] | Despite the name, searches Lemmy communities. Calls GET /api/v3/search?type_=Communities&sort=Active&q={word}. title and name are both the community title; the id is a JSON number. |
MCP:
{
"integrationId": "int_...",
"methodName": "subreddits",
"dataSchema": [{ "key": "word", "value": "rust" }]
}
The function logs in again using the stored credentials; the access_token
argument is ignored.
Important type mismatch:
subredditsreturnsidas a number, butsettings_schema()andvalidate_settings()requiresubreddit[].value.idto be a string. If you take the tool output verbatim, validation fails withid is required and must be a string. Convert the id to a string ("7", not7) before scheduling.
Also note that word is interpolated into the query string without URL
encoding. A keyword with spaces or & can produce a broken URL; use a single
word, or pre-encode it.
Post Settings & Media Validation
settings_schema()
CLI note: the simple
rustiz posts create --channel <id> --content "..."form cannot pass provider settings. Userustiz posts create --json post.jsonwith aCreatePostDto(type,date,posts[].settings, …) when any of these fields are required.
| Field | Type | Required | Validation / behavior |
|---|---|---|---|
subreddit | array | Yes | Historical name; it means Lemmy communities. minItems: 1; one post is created in each entry. |
subreddit[].value | object | Yes | Required wrapper. |
subreddit[].value.subreddit | string | Yes | Community name for display. minLength: 2. It is not used to resolve the community at publish time; id is. |
subreddit[].value.id | string | Yes | Numeric Lemmy community id, as a string. Get it from the subreddits tool and stringify it. |
subreddit[].value.title | string | Yes | Title of the post in that community. minLength: 2; each community can have a different title. |
subreddit[].value.url | string | No | Optional link-post URL. A bare domain gets https:// prepended; an http(s) URL is sent as-is. |
Example object form:
{
"subreddit": [
{
"value": {
"subreddit": "rust",
"id": "7",
"title": "Rust news",
"url": "https://example.com/article"
}
},
{
"value": {
"subreddit": "lemmy",
"id": "8",
"title": "Lemmy news"
}
}
]
}
MCP settings array:
{
"settings": [
{
"key": "subreddit",
"value": [
{
"value": {
"subreddit": "rust",
"id": "7",
"title": "Rust news",
"url": "https://example.com/article"
}
}
]
}
]
}
The engine adds "__type": "lemmy" automatically if missing.
validate_settings() rules
subredditmust exist and be an array with at least one item.- For each item at index
i:valuemust exist;value.subredditmust be a string of at least 2 characters;value.idmust be a string (numbers fail);value.titlemust be a string of at least 2 characters;value.url, if present and a string, must be a validhttp(s)URL with a host when non-empty.
Publish behavior
For each subreddit[].value, post() calls POST /api/v3/post with:
{
"community_id": 7,
"name": "Rust news",
"body": "<post message>",
"url": "https://example.com/article",
"custom_thumbnail": "<first media path>",
"nsfw": false
}
community_idis converted from a numeric string (or number) to a JSON number. A non-numeric id becomesnull, and Lemmy rejects the request.urlis included only when non-empty. A value that does not contain"http"getshttps://prepended.custom_thumbnailis the first media item’s path, regardless of its type. It must be a URL the Lemmy instance can fetch; local Rustiz paths are not uploaded to Lemmy.nsfwis alwaysfalse; there is no setting to mark a post NSFW.- The response ids and URLs are comma-joined:
post_id = "100,101",release_url = "https://instance/post/100,https://instance/post/101". - Status is
PostStatus::Success(notCompleted).
Comments
comment() is supported and is how threads/replies are published:
post_idmay be a comma-separated list from a cross-community post.- For each id, it calls
POST /api/v3/commentwith{"post_id": <number>, "content": "<comment message>"}. - Comment ids and URLs are comma-joined.
last_comment_idis ignored: every comment is a top-level comment, not a reply to the previous one.- Comment media and settings are ignored; only the text is sent.
supports_comments = truemakes this provider usable for multi-part “threads”, but each part is a separate top-level comment rather than a nested chain.
Media requirements
check_validity()looks only at the first media item of the first post:- the path must contain
png,jpg,jpef(sic) orgif; - otherwise it returns
You can set only one picture for a cover.
- the path must contain
- Because the check is a case-sensitive substring test:
.jpeg,.webp,.PNG,.JPGare rejected even though they are valid images;.jpegfails because the code checks the typojpef, notjpeg;- a path with the letters in a query string can pass accidentally.
- It does not enforce a single item: two
.jpgfiles pass and only the first is used ascustom_thumbnail. - Videos (
.mp4) are rejected as a cover. - There is no upload step. The cover/thumbnail must be a public URL the instance can reach.
Plugs
No @Plug or @PostPlug methods. run_plug() and run_post_plug() use the
trait defaults and return ProviderError::Unsupported.
Implementation Notes & Gotchas
JWT_SECRETis an undeclared hard dependency.required_env()is empty, but decryption fails without the same secret used at connect time. This is the most common cause of a Lemmy channel that connected but cannot post.- Login happens on every publish.
post(),comment()andsubreddits()callget_jwt_and_service(), which decrypts the password and performs a fresh/api/v3/user/login. The JWT stored duringauthenticate()is effectively unused (the_access_tokenargument is ignored). This means a password change breaks posting immediately, but a short-lived JWT does not. - The password is in the request body of every login. The code
deliberately strips the request body from provider errors
(
without_request_body) so the password is not stored in the engine’s error record. Keep that behavior if you refactor. - Use the username, not the email. Login accepts
username_or_email, but the subsequentGET /api/v3/user?username={identifier}does not resolve an email. A user who enters an email can getInvalid credentialseven with the right password. - 2FA is unsupported. There is no
totp_2fa_tokenin the login payload. Disable 2FA on the account or use a dedicated account. - API v3 is hard-coded. Lemmy 1.0’s v3 compatibility layer currently covers browsing and commenting, but new v4-only features are not available. A future removal of v3 would require a provider update.
- The community id type is inconsistent. The
subredditstool returns a numeric id; validation requires a string. This is a real round-trip bug, not a documentation nuance. subredditsreturnstitletwice.titleandnameare bothcommunity.title, so the tool cannot show the community’s handle (!rust@lemmy.ml) separately. Use the id for the actual post.- Search is not URL-encoded.
q={word}is concatenated directly. A space or&can break the request; use a single keyword. - Only the first attachment is used. Extra images are silently ignored
(unlike Tumblr/Dribbble, no media-count error is raised). The first path is
sent as
custom_thumbnailand is not uploaded. nsfwis always false. There is no NSFW setting; posting NSFW content to a community that requires the flag will be rejected or moderated.- Comments are flat.
last_comment_idis ignored, so a multi-part thread becomes several top-level comments rather than replies to one another. - Status is
Success, notCompleted. This differs from the article providers and Dribbble. The engine treatsSuccessas a published state; it is not a pending response. - Error mapping is instance-specific.
rate_limit_errorretries,not_logged_in/incorrect_loginrequests a refresh (which cannot help because there is no refresh token; reconnect), and bans, missing communities, blocked URLs, deleted posts and locked posts become actionablebad_bodymessages. - No refresh.
refresh_token()returns an empty token; credentials never expire, and posting re-logs in anyway. Reconnect after a password change.
References
- Lemmy API documentation (OpenAPI, v4): https://join-lemmy.org/api/main
- Lemmy API v4 upgrade guide (v3 → v4 endpoint changes and v3 compatibility): https://join-lemmy.org/docs/contributors/09-api-v4.html
- Lemmy 1.0 compatibility note: https://join-lemmy.org/news/2026-05-19_-_Open_beta_testing_for_Lemmy_1.0.0
- Postiz provider settings: https://docs.postiz.com/public-api/providers/lemmy
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview