feat: mewe

This commit is contained in:
Nevo David 2026-02-14 09:32:26 +07:00
parent 80d8804f5a
commit 683d4c3682
9 changed files with 425 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -77,6 +77,19 @@ export const ContinueIntegration: FC<{
};
}
if (provider === 'mewe') {
const hash =
typeof window !== 'undefined'
? window.location.hash.substring(1)
: '';
const hashParams = new URLSearchParams(hash);
return {
state: hashParams.get('state') || searchParams.state || '',
code: hashParams.get('loginRequestToken') || '',
refresh: searchParams.refresh || '',
};
}
return searchParams;
}, []);

View file

@ -0,0 +1,62 @@
'use client';
import { FC, useEffect, useState } from 'react';
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
export const MeweGroupSelect: FC<{
name: string;
onChange: (event: {
target: {
value: string;
name: string;
};
}) => void;
}> = (props) => {
const { onChange, name } = props;
const t = useT();
const customFunc = useCustomProviderFunction();
const [groups, setGroups] = useState([]);
const { getValues } = useSettings();
const [currentGroup, setCurrentGroup] = useState<string | undefined>();
const onChangeInner = (event: {
target: {
value: string;
name: string;
};
}) => {
setCurrentGroup(event.target.value);
onChange(event);
};
useEffect(() => {
customFunc.get('groups').then((data) => setGroups(data));
const settings = getValues()[props.name];
if (settings) {
setCurrentGroup(settings);
}
}, []);
if (!groups.length) {
return null;
}
return (
<Select
name={name}
label="Select Group"
onChange={onChangeInner}
value={currentGroup}
>
<option value="">{t('select_1', '--Select--')}</option>
{groups.map((group: any) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</Select>
);
};

View file

@ -0,0 +1,30 @@
'use client';
import {
PostComment,
withProvider,
} from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
import { FC } from 'react';
import { MeweDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/mewe.dto';
import { MeweGroupSelect } from '@gitroom/frontend/components/new-launch/providers/mewe/mewe.group.select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const MeweComponent: FC = () => {
const form = useSettings();
return (
<div>
<MeweGroupSelect {...form.register('group')} />
</div>
);
};
export default withProvider({
postComment: PostComment.POST,
comments: false,
minimumCharacters: [],
SettingsComponent: MeweComponent,
CustomPreviewComponent: undefined,
dto: MeweDto,
checkValidity: undefined,
maximumCharacters: 63206,
});

View file

@ -38,6 +38,7 @@ import GmbProvider from '@gitroom/frontend/components/new-launch/providers/gmb/g
import MoltbookProvider from '@gitroom/frontend/components/new-launch/providers/moltbook/moltbook.provider';
import SkoolProvider from '@gitroom/frontend/components/new-launch/providers/skool/skool.provider';
import WhopProvider from '@gitroom/frontend/components/new-launch/providers/whop/whop.provider';
import MeweProvider from '@gitroom/frontend/components/new-launch/providers/mewe/mewe.provider';
export const Providers = [
{
@ -167,7 +168,11 @@ export const Providers = [
{
identifier: 'whop',
component: WhopProvider,
}
},
{
identifier: 'mewe',
component: MeweProvider,
},
];
export const ShowAllProviders = forwardRef((props, ref) => {
const { date, current, global, selectedIntegrations, allIntegrations } =

View file

@ -23,6 +23,7 @@ import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-sett
import { MoltbookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/moltbook.dto';
import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto';
import { WhopDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/whop.dto';
import { MeweDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/mewe.dto';
export type ProviderExtension<T extends string, M> = { __type: T } & M;
export type AllProvidersSettings =
@ -57,6 +58,7 @@ export type AllProvidersSettings =
| ProviderExtension<'moltbook', MoltbookDto>
| ProviderExtension<'vk', None>
| ProviderExtension<'skool', SkoolDto>
| ProviderExtension<'mewe', MeweDto>
| ProviderExtension<'whop', WhopDto>;
type None = NonNullable<unknown>;
@ -95,6 +97,7 @@ export const allProviders = (setEmpty?: any) => {
{ value: MoltbookDto, name: 'moltbook' },
{ value: SkoolDto, name: 'skool' },
{ value: WhopDto, name: 'whop' },
{ value: MeweDto, name: 'mewe' },
].filter((f) => f.value);
};

View file

@ -0,0 +1,12 @@
import { IsDefined, IsString, MinLength } from 'class-validator';
import { JSONSchema } from 'class-validator-jsonschema';
export class MeweDto {
@MinLength(1)
@IsDefined()
@IsString()
@JSONSchema({
description: 'Group must be an id',
})
group: string;
}

View file

@ -35,6 +35,7 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
import { MoltbookProvider } from '@gitroom/nestjs-libraries/integrations/social/moltbook.provider';
import { SkoolProvider } from '@gitroom/nestjs-libraries/integrations/social/skool.provider';
import { WhopProvider } from '@gitroom/nestjs-libraries/integrations/social/whop.provider';
import { MeweProvider } from '@gitroom/nestjs-libraries/integrations/social/mewe.provider';
export const socialIntegrationList: Array<SocialAbstract & SocialProvider> = [
new XProvider(),
@ -69,6 +70,7 @@ export const socialIntegrationList: Array<SocialAbstract & SocialProvider> = [
new MoltbookProvider(),
new WhopProvider(),
new SkoolProvider(),
new MeweProvider(),
// new MastodonCustomProvider(),
];

View file

@ -0,0 +1,297 @@
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import dayjs from 'dayjs';
import { Integration } from '@prisma/client';
import { MeweDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/mewe.dto';
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
export class MeweProvider extends SocialAbstract implements SocialProvider {
identifier = 'mewe';
name = 'MeWe';
isBetweenSteps = false;
scopes = [] as string[];
editor = 'normal' as const;
dto = MeweDto;
private get meweHost() {
return process.env.MEWE_HOST || 'https://mewe.com';
}
private authHeaders(apiToken: string) {
return {
'X-App-Id': process.env.MEWE_APP_ID!,
'X-Api-Key': process.env.MEWE_API_KEY!,
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
};
}
maxLength() {
return 63206;
}
override handleErrors(
body: string
):
| { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
| undefined {
if (body.indexOf('Unauthorized') > -1) {
return {
type: 'refresh-token' as const,
value: 'Access token expired, please re-authenticate',
};
}
if (body.indexOf('Enhance Your Calm') > -1 || body.indexOf('420') > -1) {
return {
type: 'retry' as const,
value: 'Rate limited, retrying...',
};
}
if (body.indexOf('Forbidden') > -1) {
return {
type: 'bad-body' as const,
value: 'Insufficient permissions for this action',
};
}
return undefined;
}
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
return {
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl() {
const state = makeId(6);
return {
url:
`${this.meweHost}/login` +
`?client_id=${process.env.MEWE_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/mewe`
)}` +
`&state=${state}`,
codeVerifier: makeId(10),
state,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const loginRequestToken = params.code;
if (!loginRequestToken) {
return 'No login request token received. Please try again.';
}
try {
// Exchange loginRequestToken for apiToken
const tokenResponse = await fetch(
`${this.meweHost}/api/dev/token?loginRequestToken=${loginRequestToken}`,
{
method: 'GET',
headers: {
'X-App-Id': process.env.MEWE_APP_ID!,
'X-Api-Key': process.env.MEWE_API_KEY!,
},
}
);
if (!tokenResponse.ok) {
return 'Failed to exchange token. Please try again.';
}
const tokenData = await tokenResponse.json();
if (tokenData.pending) {
return 'Login request is still pending. Please approve on MeWe and try again.';
}
if (!tokenData.apiToken) {
return 'No API token received. Please try again.';
}
const apiToken = tokenData.apiToken;
const expiresAt = tokenData.expiresAt;
// Fetch user profile
const profileResponse = await fetch(`${this.meweHost}/api/dev/me`, {
method: 'GET',
headers: this.authHeaders(apiToken),
});
if (!profileResponse.ok) {
return 'Failed to fetch MeWe profile.';
}
const profile = await profileResponse.json();
const expiresIn = expiresAt
? dayjs(expiresAt).unix() - dayjs().unix()
: dayjs().add(30, 'days').unix() - dayjs().unix();
return {
id: profile.userId,
name:
profile.name ||
`${profile.firstName || ''} ${profile.lastName || ''}`.trim(),
accessToken: apiToken,
refreshToken: '',
expiresIn,
picture: '',
username: profile.handle || '',
};
} catch (e) {
console.log(e);
return 'MeWe authentication failed. Please try again.';
}
}
@Tool({ description: 'Groups', dataSchema: [] })
async groups(
accessToken: string,
params: any,
id: string,
integration: Integration
) {
try {
const allGroups: any[] = [];
let nextUrl: string | null = `${this.meweHost}/api/dev/groups`;
while (nextUrl) {
const response = await fetch(nextUrl, {
method: 'GET',
headers: this.authHeaders(accessToken),
});
if (!response.ok) break;
const data = await response.json();
allGroups.push(...(data.groups || []));
nextUrl = data.nextPage ? `${this.meweHost}${data.nextPage}` : null;
}
return allGroups.map((group: any) => ({
id: String(group.groupId),
name: group.name,
}));
} catch (err) {
return [];
}
}
private async uploadPhoto(
accessToken: string,
mediaPath: string
): Promise<string> {
const mediaResponse = await fetch(mediaPath);
const blob = await mediaResponse.blob();
const fileName = mediaPath.split('/').pop() || 'photo.jpg';
const form = new FormData();
form.append('file', blob, fileName);
const uploadResponse = await fetch(
`${this.meweHost}/api/dev/photo/upload`,
{
method: 'POST',
headers: {
'X-App-Id': process.env.MEWE_APP_ID!,
'X-Api-Key': process.env.MEWE_API_KEY!,
Authorization: `Bearer ${accessToken}`,
},
body: form,
}
);
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
throw new Error(`Photo upload failed: ${errorText}`);
}
const uploadData = await uploadResponse.json();
return uploadData.id;
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails<MeweDto>[],
integration: Integration
): Promise<PostResponse[]> {
const [firstPost] = postDetails;
const groupId = firstPost.settings.group;
// Upload photos if present (exclude videos)
const imageMedia =
firstPost.media?.filter((m) => !m.path || m.path.indexOf('mp4') === -1) ||
[];
const uploadedPhotoIds: string[] = [];
for (const media of imageMedia) {
const photoId = await this.uploadPhoto(accessToken, media.path);
uploadedPhotoIds.push(photoId);
}
const postBody: Record<string, any> = { text: firstPost.message };
if (uploadedPhotoIds.length > 0) {
postBody.uploadedPhotoIds = uploadedPhotoIds;
}
// MeWe post endpoint may return 204 (no content), so use raw fetch
const postResponse = await fetch(
`${this.meweHost}/api/dev/group/${groupId}/post`,
{
method: 'POST',
headers: this.authHeaders(accessToken),
body: JSON.stringify(postBody),
}
);
if (!postResponse.ok) {
const errorText = await postResponse.text();
console.log(errorText);
const handleError = this.handleErrors(errorText);
if (handleError) {
throw new Error(handleError.value);
}
throw new Error('Failed to create MeWe post');
}
let postId = '';
try {
const responseData = await postResponse.json();
postId = responseData.postId || responseData.id || makeId(12);
} catch {
postId = makeId(12);
}
return [
{
id: firstPost.id,
postId,
releaseURL: `${this.meweHost}/group/${groupId}`,
status: 'success',
},
];
}
}