From 683d4c36829d3abc49fcdc2e53e5e0a94437067f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 14 Feb 2026 09:32:26 +0700 Subject: [PATCH] feat: mewe --- apps/frontend/public/icons/platforms/mewe.png | Bin 0 -> 2814 bytes .../launches/continue.integration.tsx | 13 + .../providers/mewe/mewe.group.select.tsx | 62 ++++ .../providers/mewe/mewe.provider.tsx | 30 ++ .../providers/show.all.providers.tsx | 7 +- .../all.providers.settings.ts | 3 + .../dtos/posts/providers-settings/mewe.dto.ts | 12 + .../src/integrations/integration.manager.ts | 2 + .../src/integrations/social/mewe.provider.ts | 297 ++++++++++++++++++ 9 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/public/icons/platforms/mewe.png create mode 100644 apps/frontend/src/components/new-launch/providers/mewe/mewe.group.select.tsx create mode 100644 apps/frontend/src/components/new-launch/providers/mewe/mewe.provider.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/mewe.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/social/mewe.provider.ts diff --git a/apps/frontend/public/icons/platforms/mewe.png b/apps/frontend/public/icons/platforms/mewe.png new file mode 100644 index 0000000000000000000000000000000000000000..0f0f10343d1fbe9c75780bb0d3a84fa745ff5f2c GIT binary patch literal 2814 zcmV|O7A@7I}mFXxtT zw&ec^M*`7%ZN={ak$kT$_!wf40g&xSmlGMiXlcDgb*05u~HsFzROBuh>Ia!bsFh96es5p zmb}JDV+?L}8q2f600x5D#l!*cxbnJ0bQs~)W$FFP`^fgRGIRQE&i(7Ji1Wd!o>9e+ z#$B7a>ES-?3Xid4XBhmqXE91&A#V&?8Mb`x0Nsc7V0jLg``_TTfBZSY+*0XYV-jcV z{F{fV?rp_r&D`K9{a^h~!ss1L)+AzLg;!Ta-w*$qrd`|6Izq}m>hJ93xj+3z=|L-q ziy;jk=w;uxzl>W|fsPdIAH9=wdjl{3?GLe*iNyJUn?ALl+rIJ;@lt?Mik=7eV)+io zzxv&hjV4x9w$_vF*?{yMY}aP#(j?JR0XyYgW$T*79v1?-AJ|LNu5AQ!^MvzxiZcsz ze*A;9-gi4HAH0h>Z26Oq;Z{`;&Mpxz76|8-=svWEs*Nq^D8^`wo2}%gKiW@R2v9+Y zj$(?li)`3?3w7JOP(hevL#)X5w&G+`=rBS@imB7*6Qb|J+FeUZYPWZzb(9pFF`zX@ zYkK~0A3|RFx++H0ZSSIG?>3@^0$?!4U}BA(^2l^HqNAkNTKDfDyKw`m7$y-F5|ls7K<2dFiK-*Qgk1>8y&{z zNRjDkqW!=fsCTcagTnw>1mrKvS%GvM!63cTj zk;1F3qV2=C6XgT^hAim~b?7KYSQc|{jSw#6ukCTbr%R-)rXYi7<~T(v&dj!3LXcQxT;Qp-JC zF!tPO&L8f_a&2@NQQ6)=>xXZ{sr2Z+e-AnclW2@hGx+1*P?(rUNQsUWetnjfd$!ZK zb2BO*V0jLcr`}@n)F8pk0wYIHAzcR*#$-C1XxhDns*Wa*SJHXr+(pDHEq`qy%23%_ zS1M>#2v{1K;=)rW67iA}9Vt2v?xg$BUaB@Wqlyuh=W_AsmzcjW#^U+0L=aVH^rFHTX$kU|XULBwQTtyJK|rRv zg_T%F3q``&CFTb&G4fxpVEHbp5KyGg!Wh<=V^+3^RW6O{C|biXnAdI?497p@S%?kHJd>^ZBbf<@Jde zuv~|1Z!0=hNJ~-}pCOo3nM4

7!A22Co!>F&WNNgR>qQAK zLT;RJehItMP$=Ly&K0MK zWCN8_Vp$f&Vv(Af8f@Dp2!b`qthrd}SaJK;9tI(J@$28C@y^ZM_LYbD?bpADU!Uc+ zFMpE$zxroV4OzNBaTmXN{M#Hj@KHYV$Dd(jWEju$$>nmiwzeXrfM`cSb$G-F}T6S%r<0E%)`Q%xI5X4F`I5-GEE|+6q;2dBW85tpy$>6#! z*=&ZkwpRN4k8|?mNvf->*}Z!=-~8scc;JCUOifKOGc$wdxpa1R62}oIPMpBD%O}v9 z({J|XcA|wMuRie;n(x}m`#!U?vpn<6QEF@JOGXV0oWmHyrcJ#(`Q(%A+_{s>m&bYGg?@&H-bPAEOG^t= zQ`2ikqoSy{o+qrYZhVS)DcHYzGAID7U@cI>#7 z;o%XcrlzQ`ujk%-?_qp=oc{jfYel5Ijvx|+H*vC_s$y8u5*0+H*CtlU3hO!uA&8=6 zzjs}iC`txK$8iY5u+)tTg#yMHeBZ~iETSkvYmMi56pKY1$HB5J;y9*QEMi#}zVDZf z#@a;QUzq-X^V{zYBI^w|h^#l void; +}> = (props) => { + const { onChange, name } = props; + const t = useT(); + const customFunc = useCustomProviderFunction(); + const [groups, setGroups] = useState([]); + const { getValues } = useSettings(); + const [currentGroup, setCurrentGroup] = useState(); + + 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 ( + + ); +}; diff --git a/apps/frontend/src/components/new-launch/providers/mewe/mewe.provider.tsx b/apps/frontend/src/components/new-launch/providers/mewe/mewe.provider.tsx new file mode 100644 index 00000000..8aadca6c --- /dev/null +++ b/apps/frontend/src/components/new-launch/providers/mewe/mewe.provider.tsx @@ -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 ( +

+ +
+ ); +}; + +export default withProvider({ + postComment: PostComment.POST, + comments: false, + minimumCharacters: [], + SettingsComponent: MeweComponent, + CustomPreviewComponent: undefined, + dto: MeweDto, + checkValidity: undefined, + maximumCharacters: 63206, +}); diff --git a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx index 3ade8870..91844429 100644 --- a/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/new-launch/providers/show.all.providers.tsx @@ -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 } = diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts index 071be782..a66261e1 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts @@ -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 = { __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; @@ -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); }; diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/mewe.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/mewe.dto.ts new file mode 100644 index 00000000..f1039bae --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/mewe.dto.ts @@ -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; +} diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index 731bdbe5..a09ea2f6 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -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 = [ new XProvider(), @@ -69,6 +70,7 @@ export const socialIntegrationList: Array = [ new MoltbookProvider(), new WhopProvider(), new SkoolProvider(), + new MeweProvider(), // new MastodonCustomProvider(), ]; diff --git a/libraries/nestjs-libraries/src/integrations/social/mewe.provider.ts b/libraries/nestjs-libraries/src/integrations/social/mewe.provider.ts new file mode 100644 index 00000000..55640527 --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/mewe.provider.ts @@ -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 { + 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 { + 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[], + integration: Integration + ): Promise { + 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 = { 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', + }, + ]; + } +}