Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

PropertyValue
Identifierlemmy
Display nameLemmy
Connection modecustomFields (instance URL, identifier, password)
Editornormal
Channel kindsocial
Comments / threadsYes. supports_comments = true; each comma-separated post id gets a top-level comment
Pending / async publishingNo. supports_pending = false; returns PostStatus::Success
AnalyticsNo. supports_analytics = false
Post analyticsNo. supports_post_analytics = false
MentionsNo. supports_mentions = false
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job3
Character limit (max_length)10000 characters for the post body
APILemmy 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.

VariableRequiredDefaultPurpose
JWT_SECRETNo, 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_NETWORKNofalseGlobal 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.

  1. Choose the Lemmy instance (for example https://lemmy.world) and create an account there, or use an existing account.

  2. 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.

  3. 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.

  4. Do not enable two-factor authentication on the account. Rustiz’s login body sends only username_or_email and password; it has no totp_2fa_token field, so a 2FA-enabled account cannot authenticate.

  5. Use the username as the identifier, not the email address. The login endpoint accepts either, but the follow-up user lookup calls GET /api/v3/user?username={identifier}; an email in that query does not resolve to a user and the connect fails with Invalid credentials.

  6. 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/name and avatar from 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/login
  • GET /api/v3/user?username=...
  • POST /api/v3/post
  • POST /api/v3/comment
  • GET /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:

KeyTypeValidation regexNotes
servicetext/^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.
identifiertext/^.{3,}$/Username (preferred) or email. At least 3 characters.
passwordpassword/^.{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)

FunctionParametersReturnsNotes
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: subreddits returns id as a number, but settings_schema() and validate_settings() require subreddit[].value.id to be a string. If you take the tool output verbatim, validation fails with id is required and must be a string. Convert the id to a string ("7", not 7) 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. Use rustiz posts create --json post.json with a CreatePostDto (type, date, posts[].settings, …) when any of these fields are required.

FieldTypeRequiredValidation / behavior
subredditarrayYesHistorical name; it means Lemmy communities. minItems: 1; one post is created in each entry.
subreddit[].valueobjectYesRequired wrapper.
subreddit[].value.subredditstringYesCommunity name for display. minLength: 2. It is not used to resolve the community at publish time; id is.
subreddit[].value.idstringYesNumeric Lemmy community id, as a string. Get it from the subreddits tool and stringify it.
subreddit[].value.titlestringYesTitle of the post in that community. minLength: 2; each community can have a different title.
subreddit[].value.urlstringNoOptional 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

  1. subreddit must exist and be an array with at least one item.
  2. For each item at index i:
    • value must exist;
    • value.subreddit must be a string of at least 2 characters;
    • value.id must be a string (numbers fail);
    • value.title must be a string of at least 2 characters;
    • value.url, if present and a string, must be a valid http(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_id is converted from a numeric string (or number) to a JSON number. A non-numeric id becomes null, and Lemmy rejects the request.
  • url is included only when non-empty. A value that does not contain "http" gets https:// prepended.
  • custom_thumbnail is 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.
  • nsfw is always false; 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 (not Completed).

Comments

comment() is supported and is how threads/replies are published:

  • post_id may be a comma-separated list from a cross-community post.
  • For each id, it calls POST /api/v3/comment with {"post_id": <number>, "content": "<comment message>"}.
  • Comment ids and URLs are comma-joined.
  • last_comment_id is 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 = true makes 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) or gif;
    • otherwise it returns You can set only one picture for a cover.
  • Because the check is a case-sensitive substring test:
    • .jpeg, .webp, .PNG, .JPG are rejected even though they are valid images;
    • .jpeg fails because the code checks the typo jpef, not jpeg;
    • a path with the letters in a query string can pass accidentally.
  • It does not enforce a single item: two .jpg files pass and only the first is used as custom_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_SECRET is 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() and subreddits() call get_jwt_and_service(), which decrypts the password and performs a fresh /api/v3/user/login. The JWT stored during authenticate() is effectively unused (the _access_token argument 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 subsequent GET /api/v3/user?username={identifier} does not resolve an email. A user who enters an email can get Invalid credentials even with the right password.
  • 2FA is unsupported. There is no totp_2fa_token in 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 subreddits tool returns a numeric id; validation requires a string. This is a real round-trip bug, not a documentation nuance.
  • subreddits returns title twice. title and name are both community.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_thumbnail and is not uploaded.
  • nsfw is 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_id is ignored, so a multi-part thread becomes several top-level comments rather than replies to one another.
  • Status is Success, not Completed. This differs from the article providers and Dribbble. The engine treats Success as a published state; it is not a pending response.
  • Error mapping is instance-specific. rate_limit_error retries, not_logged_in/incorrect_login requests a refresh (which cannot help because there is no refresh token; reconnect), and bans, missing communities, blocked URLs, deleted posts and locked posts become actionable bad_body messages.
  • No refresh. refresh_token() returns an empty token; credentials never expire, and posting re-logs in anyway. Reconnect after a password change.

References