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

MeWe (mewe)

MeWe is a social network with personal timelines and groups. Rustiz connects a MeWe account through MeWe’s standalone-app OAuth flow, then publishes text + photos either to the user’s timeline or to a group. There is no native comment/thread support in this provider.

  • Rust source: crates/rustiz-providers/src/mewe.rs
  • Upstream port: mewe.provider.ts
  • Editor: Normal (plain text)
  • Connection mode: oauth

Overview

PropertyValue
Identifiermewe
Display nameMeWe
Connection modeoauth
Editornormal
Channel kindsocial
Comments / threadsNo. supports_comments is the default false; comment() is not implemented
Pending / async publishingNo. supports_pending = false; post() returns PostStatus::Success
AnalyticsNo
Post analyticsNo
MentionsNo
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job1 (default)
Character limit (max_length)63_206
API hosthttps://mewe.com (MEWE_BASE_URL, or MEWE_HOST)
API styleREST under /api/dev/..., app headers + bearer token

Server Environment Variables

required_env() returns two variables:

#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
    &["MEWE_APP_ID", "MEWE_API_KEY"]
}
}

required_env_alternatives() is not overridden.

VariableRequiredDefaultPurpose
MEWE_APP_IDYes""The app id (X-App-Id) from the MeWe developer portal.
MEWE_API_KEYYes""The app’s secret API key (X-Api-Key).
MEWE_BASE_URLNo—Preferred host override; base_url("MEWE", ...) reads MEWE_BASE_URL first and strips trailing slashes.
MEWE_HOSTNohttps://mewe.comFallback host used only when MEWE_BASE_URL is unset. This is a legacy/default name, not the *_BASE_URL override.
FRONTEND_URLNohttp://localhost:5000Public origin; the OAuth redirect URI is derived from it. Trailing slashes are stripped.
JWT_SECRETNo""Not used by this OAuth provider (no encrypted client info or custom fields). Set it for the rest of the deployment.

Host precedence in code:

#![allow(unused)]
fn main() {
self.ctx.config.base_url(
    "MEWE",
    self.ctx.config.get_or("MEWE_HOST", "https://mewe.com"),
)
}

So MEWE_BASE_URL wins over MEWE_HOST, which wins over https://mewe.com.

rustiz providers --status reports MEWE_APP_ID and MEWE_API_KEY (never their values) and marks MeWe configured only when both are non-empty.

Step-by-Step Developer App Setup

1. Join the MeWe Developer Program

Portal: https://developer.mewe.com/developer (developer settings), after reading https://developer.mewe.com/.

MeWe’s Open API is a developer preview. Sign in with a MeWe account, apply to the developer program, and wait for approval. The program is in beta with limited spots; an unapproved account cannot create an app or obtain an API key. There is no self-serve “create app and use it immediately” path.

2. Create a Standalone app

In developer settings, create an application and choose the Standalone type (MeWe also documents Embedded apps and, as “coming soon”, Agents; Rustiz uses the standalone flow because it runs on a backend).

Copy:

  • the App ID → MEWE_APP_ID;
  • the API Key → MEWE_API_KEY.

3. Add the exact redirect URI

generate_auth_url() builds:

{MEWE_HOST}/login?client_id={MEWE_APP_ID}&redirect_uri={urlencode(FRONTEND_URL/integrations/social/mewe)}&state={state}

frontend_url() strips trailing slashes, so register exactly:

{FRONTEND_URL}/integrations/social/mewe

Examples:

https://social.example.com/integrations/social/mewe
http://localhost:5000/integrations/social/mewe

There is no trailing slash after mewe, and the provider does not wrap the URI in redirectmeto.com. Use the same http/https scheme, host and port in the MeWe app settings.

Rustiz sends this exact login URL (the redirect URI is URL-encoded; there is no scope or response_type parameter):

{MEWE_HOST}/login?client_id={MEWE_APP_ID}&redirect_uri={urlencode(FRONTEND_URL/integrations/social/mewe)}&state={state}

4. Configure permissions

The MeWe authorize URL has no scope parameter. Permissions are configured on the app in the developer portal and approved by the user during the login consent screen.

The endpoints Rustiz calls need, at minimum:

CapabilityEndpoint usedPermission family
Read the signed-in profileGET /api/dev/meprofile / basic user info
List groupsGET /api/dev/groupsgroups read (MeWe’s group-agent docs call this user_groups)
Post to the timelinePOST /api/dev/me/postpost/create content (post_content)
Post to a groupPOST /api/dev/group/{groupId}/postgroups + post/create content
Upload a photoPOST /api/dev/photo/uploadphoto/media upload

MeWe’s public docs use permission names such as post_content and user_groups in the Group Agent section. The portal’s exact labels can differ; enable the posting, group and photo-upload permissions the portal offers.

check_scopes() is not called in mewe.rs, and there is no scope check after the callback. A missing app permission appears only when the corresponding API call is made.

5. OAuth flow used by Rustiz

  1. Browser goes to {MEWE_HOST}/login?client_id=...&redirect_uri=...&state=....

  2. The user signs in and approves the app.

  3. MeWe redirects to the callback with loginRequestToken and state:

    https://social.example.com/integrations/social/mewe?loginRequestToken=abc123&state=xyz789
    
  4. Rustiz exchanges the token:

    GET {MEWE_HOST}/api/dev/token?loginRequestToken=abc123
    X-App-Id: <MEWE_APP_ID>
    X-Api-Key: <MEWE_API_KEY>
    

    Successful response:

    { "expiresAt": "2026-12-06T12:00:00.000Z", "apiToken": "..." }
    

    A pending: true response means the user has not approved an in-app request yet; Rustiz returns “Login request is still pending. Please approve on MeWe and try again.”

  5. Rustiz fetches GET /api/dev/me with Authorization: Bearer <apiToken> plus the app headers, and stores userId, name/firstName+lastName, and handle.

6. Sandbox / testing mode vs production

MeWe documents no separate sandbox host for third-party apps. The API communication page lists production as https://mewe.com; it does not document a distinct sandbox base URL. Plan to test with:

  • a dedicated MeWe test account,
  • a test group where the account is a member/admin,
  • and the same production app/API key.

The main gate is the MeWe developer-program application review. Once the developer account and app are approved and the user consents, the same credentials work in production. Publishing/listing the app in the MeWe Store is a separate, optional step.

Important callback mismatch in this port

The generic Rustiz callback (crates/rustiz-server/src/oauth.rs) reads code, state and error from the query:

#![allow(unused)]
fn main() {
struct CallbackQuery {
    code: Option<String>,
    state: Option<String>,
    error: Option<String>,
}
}

MeWe sends loginRequestToken, not code. As written, a real MeWe login lands on /integrations/social/mewe?loginRequestToken=...&state=... and the server responds Missing code or state; rustiz channels connect mewe then waits until its timeout.

Workarounds, in order of preference:

  1. Rewrite the callback URL before it is consumed. After MeWe redirects, change the query key loginRequestToken to code and reload:

    https://social.example.com/integrations/social/mewe?code=abc123&state=xyz789
    

    The state row still exists, and authenticate() receives the login request token as params.code. This is the simplest way to finish the connection with the current binary.

  2. Add a tiny proxy/frontend route at /integrations/social/mewe that forwards loginRequestToken as code to the Rustiz callback.

  3. Patch the server callback to accept loginRequestToken as an alias for code. This is the durable fix, but it is outside this provider’s file.

If MeWe ever returns both code and loginRequestToken, the generic callback works unchanged. The official documented success shape is only loginRequestToken + state.

Connecting a Channel (CLI & MCP)

CLI

rustiz channels connect mewe

Optional:

rustiz channels connect mewe --open
rustiz channels connect mewe --timeout 600

Do not use --fields or --external-url:

  • --fields is the customFields path; it base64-encodes JSON and passes it to authenticate() as the OAuth code. MeWe expects a loginRequestToken, so the connection fails.
  • --external-url calls external_url(), which MeWe does not implement.

Because of the callback mismatch above, expect the CLI to print the login URL and then time out unless you rewrite the callback query (or use a proxy).

MCP

Start:

{ "provider": "mewe" }

Poll:

{ "connectionId": "Ab12Cd" }

connectChannelTool returns { connected: false, pending: true, connectionId, url }. The human opens the URL. After MeWe redirects, apply the loginRequestToken → code rewrite if necessary, then poll with the same connectionId and check integrationList.

No customFields, externalUrl, name or description for MeWe. custom_fields() is not overridden, so integrationSchema.customFields is an empty array: there are no keys or validation regexes.

Selecting a group (not channelSetupTool)

is_between_steps = false, so the channel is saved immediately and there is no channelSetupTool step. The target is chosen per post with settings.postType / settings.group.

One helper is exposed through triggerTool:

FunctionParametersReturns
groups{} (ignored)[{"id": "<groupId>", "name": "<group name>"}]

groups calls GET /api/dev/groups with the stored bearer token and follows nextPage until it is empty. Any network/status/JSON error stops the loop and returns the groups collected so far, so a partial or empty list does not necessarily mean the account has no groups.

MCP:

{
  "integrationId": "int_...",
  "methodName": "groups",
  "dataSchema": []
}

The returned id is the value to put in settings.group when postType is group. Numeric ids are stringified, so the result is always a string suitable for validate_settings().

Post Settings & Media Validation

settings_schema()

FieldTypeRequiredValidation / behavior
postTypestring enumYes"timeline" or "group".
groupstringConditionalRequired when postType == "group"; minLength: 1. Must be the numeric/string group id, not the name. Ignored for timeline posts.

additionalProperties: true.

Example timeline:

{ "__type": "mewe", "postType": "timeline" }

Example group:

{ "__type": "mewe", "postType": "group", "group": "60ffe9492f19cd2677b0f1a5" }

Use rustiz posts create --json post.json; the simple rustiz posts create --channel ... --content ... form cannot pass provider settings. MCP:

{
  "settings": [
    { "key": "postType", "value": "group" },
    { "key": "group", "value": "60ffe9492f19cd2677b0f1a5" }
  ]
}

validate_settings() rules

  1. postType must be exactly "timeline" or "group".
  2. If postType == "group", group must be a non-empty string.
  3. No other fields are validated.

post() itself defaults a missing postType to "group" and reads group as "", so a direct provider call that bypasses validation can build /api/dev/group//post and fail at MeWe.

Publish behavior

post() uses only the first PostDetails:

  • postType decides the endpoint:

    • timeline → POST {host}/api/dev/me/post
    • group → POST {host}/api/dev/group/{group}/post
  • Body:

    { "text": "<message>", "uploadedPhotoIds": ["<photo id>", "..."] }
    

    uploadedPhotoIds is omitted when there are no photos. MeWe’s reference renders this field as a string in one example; Rustiz sends a JSON array of photo ids, which is the natural shape for multiple photos. If a deployment rejects the array, join the ids into a comma-separated string.

  • The endpoint may answer 204 No Content. The provider calls the raw execute() client (not the retrying JSON client), checks is_success(), and drains the body.

  • MeWe returns no post id, so Rustiz generates a random 12-character id with make_id(12) and stores it as post_id. This is not a real MeWe id.

  • release_url:

    • timeline → https://mewe.com/{integration.profile}/posts
    • group → https://mewe.com/group/{group}

    integration.profile is the stored handle; if it is empty the timeline URL becomes https://mewe.com//posts.

  • Status is PostStatus::Success.

Comments / threads

No. supports_comments = false; comment() is the trait default and returns ProviderError::Unsupported. There is no reply/thread support.

Media requirements

  • No check_validity() override: any media type/count is accepted by Rustiz.

  • The post filters media with:

    #![allow(unused)]
    fn main() {
    m.path.is_empty() || !has_extension(&m.path, "mp4")
    }

    has_extension lowercases the path and checks for the substring .mp4. Therefore:

    • .mp4, .MP4, and even .mp4?x=1 are excluded (treated as video and not uploaded);
    • .mov, .webm, .m4v, .gif, .pdf, and every other extension are treated as photos and sent to /api/dev/photo/upload.
  • Photos are uploaded one at a time (not concurrently) to POST /api/dev/photo/upload as multipart field file. The response id is collected; a response without id contributes an empty string.

  • Each upload failure aborts the post with Photo upload failed: <body>.

  • Alt text and thumbnails are ignored.

  • An empty media path is not filtered out; it is passed to media_stream() and will fail.

Plugs

No @Plug or @PostPlug methods. run_plug() / run_post_plug() use the trait defaults and return ProviderError::Unsupported.

Implementation Notes & Gotchas

  • Callback parameter mismatch is the biggest issue. MeWe’s redirect uses loginRequestToken; the generic callback expects code. Without a rewrite or proxy, the connection cannot complete. See the workaround above.
  • No refresh token. refresh_token() returns an all-empty AuthTokenDetails with expires_in: 0. The engine treats an empty access token as a failed refresh, sets refresh_needed, and notifies the user to reconnect. refresh_cron = false, so there is no proactive refresh.
  • Token expiry is derived from expiresAt. A string is parsed as RFC 3339; a number is treated as milliseconds and divided by 1000. If expiresAt is missing or unparsable, the provider assumes 30 days. A past timestamp yields a negative expires_in.
  • A 401 on publish does not trigger a refresh. post() and upload_photo() use the raw execute() client and convert any handled error into ProviderError::Other, losing the refresh classification. Expired tokens therefore surface as retryable failures; reconnect the channel.
  • Publish bypasses the retry client. The 204 response is why post() does not use send_json(), but that also means a 429/500 during publish is not automatically retried. upload_photo() has the same limitation.
  • Synthetic post_id. Because MeWe returns 204, the stored post id is a random Rustiz id, not a MeWe post id. It cannot be used to fetch, comment on, or analyze the real post; release_url is the only link back.
  • Timeline release_url depends on the handle. It uses https://mewe.com/{handle}/posts; a profile without a handle produces a double slash.
  • groups swallows errors. A failed page stops pagination silently. The result can be partial.
  • nextPage is prefixed with the host unconditionally. The code does format!("{host}{p}"); if MeWe ever returns an absolute URL in nextPage, the result becomes https://mewe.comhttps://... and the next request fails.
  • groups ignores its parameters. The function schema has no properties; the MCP dataSchema is ignored. There is no search or page-size control.
  • Only .mp4 is recognized as video. Every other video container is sent to the photo endpoint and rejected by MeWe.
  • The app API key is sent on every authenticated request. MeWe’s docs say X-Api-Key is required only for authorization requests; auth_headers() sends it on GET /api/dev/me, group listing and posts too. It works, but the app secret travels on more requests than necessary.
  • Error mapping is string matching. Unauthorized → refresh, Enhance Your Calm or 420 → retry, Forbidden → bad body. The strings are case-sensitive and are not MeWe error codes. Because post() converts every handled error to ProviderError::Other, a Forbidden on publish is retried as a transient failure rather than failing fast.
  • No documented numeric rate limit. MeWe’s public docs do not publish a requests-per-minute figure. The provider recognizes “Enhance Your Calm” and bodies containing 420 as retryable, but post()/upload_photo() bypass the retrying client, so a 429 during publishing is not retried automatically.
  • picture is always empty. AuthTokenDetails.picture is Some(""), so the channel has no avatar in Rustiz.
  • Name fallback. name is used when non-empty; otherwise the provider joins firstName + lastName and trims the result. Both empty produces an empty channel name.
  • state is only 6 characters. It is stored server-side and consumed once, but it is much shorter than the 32-character state recommended by most OAuth providers.
  • The PKCE verifier is unused. generate_auth_url() stores a random 10-character code_verifier, but MeWe’s login URL has no code_challenge/code_challenge_method and the token exchange never sends it. MeWe’s standalone flow does not use PKCE; the verifier is dead weight.
  • No analytics, mentions, pending posts or plugs. The corresponding trait methods are not overridden.

References