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
| Property | Value |
|---|---|
| Identifier | mewe |
| Display name | MeWe |
| Connection mode | oauth |
| Editor | normal |
| Channel kind | social |
| Comments / threads | No. supports_comments is the default false; comment() is not implemented |
| Pending / async publishing | No. supports_pending = false; post() returns PostStatus::Success |
| Analytics | No |
| Post analytics | No |
| Mentions | No |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 1 (default) |
Character limit (max_length) | 63_206 |
| API host | https://mewe.com (MEWE_BASE_URL, or MEWE_HOST) |
| API style | REST 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
MEWE_APP_ID | Yes | "" | The app id (X-App-Id) from the MeWe developer portal. |
MEWE_API_KEY | Yes | "" | The app’s secret API key (X-Api-Key). |
MEWE_BASE_URL | No | — | Preferred host override; base_url("MEWE", ...) reads MEWE_BASE_URL first and strips trailing slashes. |
MEWE_HOST | No | https://mewe.com | Fallback host used only when MEWE_BASE_URL is unset. This is a legacy/default name, not the *_BASE_URL override. |
FRONTEND_URL | No | http://localhost:5000 | Public origin; the OAuth redirect URI is derived from it. Trailing slashes are stripped. |
JWT_SECRET | No | "" | 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:
| Capability | Endpoint used | Permission family |
|---|---|---|
| Read the signed-in profile | GET /api/dev/me | profile / basic user info |
| List groups | GET /api/dev/groups | groups read (MeWe’s group-agent docs call this user_groups) |
| Post to the timeline | POST /api/dev/me/post | post/create content (post_content) |
| Post to a group | POST /api/dev/group/{groupId}/post | groups + post/create content |
| Upload a photo | POST /api/dev/photo/upload | photo/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
-
Browser goes to
{MEWE_HOST}/login?client_id=...&redirect_uri=...&state=.... -
The user signs in and approves the app.
-
MeWe redirects to the callback with
loginRequestTokenandstate:https://social.example.com/integrations/social/mewe?loginRequestToken=abc123&state=xyz789 -
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: trueresponse 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.” -
Rustiz fetches
GET /api/dev/mewithAuthorization: Bearer <apiToken>plus the app headers, and storesuserId,name/firstName+lastName, andhandle.
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:
-
Rewrite the callback URL before it is consumed. After MeWe redirects, change the query key
loginRequestTokentocodeand reload:https://social.example.com/integrations/social/mewe?code=abc123&state=xyz789The state row still exists, and
authenticate()receives the login request token asparams.code. This is the simplest way to finish the connection with the current binary. -
Add a tiny proxy/frontend route at
/integrations/social/mewethat forwardsloginRequestTokenascodeto the Rustiz callback. -
Patch the server callback to accept
loginRequestTokenas an alias forcode. 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:
--fieldsis thecustomFieldspath; it base64-encodes JSON and passes it toauthenticate()as the OAuth code. MeWe expects aloginRequestToken, so the connection fails.--external-urlcallsexternal_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:
| Function | Parameters | Returns |
|---|---|---|
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()
| Field | Type | Required | Validation / behavior |
|---|---|---|---|
postType | string enum | Yes | "timeline" or "group". |
group | string | Conditional | Required 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
postTypemust be exactly"timeline"or"group".- If
postType == "group",groupmust be a non-empty string. - 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:
-
postTypedecides the endpoint:- timeline →
POST {host}/api/dev/me/post - group →
POST {host}/api/dev/group/{group}/post
- timeline →
-
Body:
{ "text": "<message>", "uploadedPhotoIds": ["<photo id>", "..."] }uploadedPhotoIdsis 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), checksis_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 aspost_id. This is not a real MeWe id. -
release_url:- timeline →
https://mewe.com/{integration.profile}/posts - group →
https://mewe.com/group/{group}
integration.profileis the storedhandle; if it is empty the timeline URL becomeshttps://mewe.com//posts. - timeline →
-
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_extensionlowercases the path and checks for the substring.mp4. Therefore:.mp4,.MP4, and even.mp4?x=1are 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/uploadas multipart fieldfile. The responseidis collected; a response withoutidcontributes 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 expectscode. Without a rewrite or proxy, the connection cannot complete. See the workaround above. - No refresh token.
refresh_token()returns an all-emptyAuthTokenDetailswithexpires_in: 0. The engine treats an empty access token as a failed refresh, setsrefresh_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. IfexpiresAtis missing or unparsable, the provider assumes 30 days. A past timestamp yields a negativeexpires_in. - A 401 on publish does not trigger a refresh.
post()andupload_photo()use the rawexecute()client and convert any handled error intoProviderError::Other, losing therefreshclassification. Expired tokens therefore surface as retryable failures; reconnect the channel. - Publish bypasses the retry client. The 204 response is why
post()does not usesend_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_urlis the only link back. - Timeline
release_urldepends on the handle. It useshttps://mewe.com/{handle}/posts; a profile without a handle produces a double slash. groupsswallows errors. A failed page stops pagination silently. The result can be partial.nextPageis prefixed with the host unconditionally. The code doesformat!("{host}{p}"); if MeWe ever returns an absolute URL innextPage, the result becomeshttps://mewe.comhttps://...and the next request fails.groupsignores its parameters. The function schema has no properties; the MCPdataSchemais ignored. There is no search or page-size control.- Only
.mp4is 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-Keyis required only for authorization requests;auth_headers()sends it onGET /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 Calmor420→ retry,Forbidden→ bad body. The strings are case-sensitive and are not MeWe error codes. Becausepost()converts every handled error toProviderError::Other, aForbiddenon 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
420as retryable, butpost()/upload_photo()bypass the retrying client, so a 429 during publishing is not retried automatically. pictureis always empty.AuthTokenDetails.pictureisSome(""), so the channel has no avatar in Rustiz.- Name fallback.
nameis used when non-empty; otherwise the provider joinsfirstName+lastNameand trims the result. Both empty produces an empty channel name. stateis 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-charactercode_verifier, but MeWe’s login URL has nocode_challenge/code_challenge_methodand 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
- MeWe developer portal: https://developer.mewe.com/
- MeWe quickstart / developer program: https://developer.mewe.com/quickstart
- MeWe authentication (standalone OAuth): https://developer.mewe.com/authentication
- MeWe API communication / headers: https://developer.mewe.com/api-communication
- MeWe API reference (groups, group post, timeline post, photo upload): https://developer.mewe.com/api-docs
- Postiz MeWe settings: https://docs.postiz.com/public-api/providers/mewe