feat: mewe
This commit is contained in:
parent
80d8804f5a
commit
683d4c3682
9 changed files with 425 additions and 1 deletions
BIN
apps/frontend/public/icons/platforms/mewe.png
Normal file
BIN
apps/frontend/public/icons/platforms/mewe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -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;
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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 } =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue