Skip to content

Commit 3e74b52

Browse files
authored
Merge pull request #706 from gitroomhq/feat/vk
Added VK
2 parents 7c265e5 + 0abd689 commit 3e74b52

File tree

6 files changed

+269
-0
lines changed

6 files changed

+269
-0
lines changed
14.3 KB
Loading

apps/frontend/src/app/(site)/integrations/social/[provider]/continue/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export default async function Page({
2222
};
2323
}
2424

25+
if (provider === 'vk') {
26+
searchParams = {
27+
...searchParams,
28+
state: searchParams.state || '',
29+
code: searchParams.code + '&&&&' + searchParams.device_id
30+
};
31+
}
32+
2533
const data = await internalFetch(`/integrations/social/${provider}/connect`, {
2634
method: 'POST',
2735
body: JSON.stringify(searchParams),

apps/frontend/src/components/launches/providers/show.all.providers.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy
2121
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
2222
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
2323
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
24+
import VkProvider from '@gitroom/frontend/components/launches/providers/vk/vk.provider';
2425

2526
export const Providers = [
2627
{identifier: 'devto', component: DevtoProvider},
@@ -46,6 +47,7 @@ export const Providers = [
4647
{identifier: 'wrapcast', component: WarpcastProvider},
4748
{identifier: 'telegram', component: TelegramProvider},
4849
{identifier: 'nostr', component: NostrProvider},
50+
{identifier: 'vk', component: VkProvider},
4951
];
5052

5153

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
2+
3+
export default withProvider(
4+
null,
5+
undefined,
6+
undefined,
7+
async (posts) => {
8+
return true;
9+
},
10+
2048
11+
);

libraries/nestjs-libraries/src/integrations/integration.manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrati
2626
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
2727
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
2828
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
29+
import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider';
2930

3031
export const socialIntegrationList: SocialProvider[] = [
3132
new XProvider(),
@@ -48,6 +49,7 @@ export const socialIntegrationList: SocialProvider[] = [
4849
new FarcasterProvider(),
4950
new TelegramProvider(),
5051
new NostrProvider(),
52+
new VkProvider(),
5153
// new MastodonCustomProvider(),
5254
];
5355

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import {
2+
AuthTokenDetails,
3+
PostDetails,
4+
PostResponse,
5+
SocialProvider,
6+
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
7+
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
8+
import dayjs from 'dayjs';
9+
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
10+
import { createHash, randomBytes } from 'crypto';
11+
import axios from 'axios';
12+
import FormDataNew from 'form-data';
13+
import mime from 'mime-types';
14+
15+
export class VkProvider extends SocialAbstract implements SocialProvider {
16+
identifier = 'vk';
17+
name = 'VK';
18+
isBetweenSteps = false;
19+
scopes = [
20+
'vkid.personal_info',
21+
'email',
22+
'wall',
23+
'status',
24+
'docs',
25+
'photos',
26+
'video',
27+
];
28+
29+
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
30+
return {
31+
refreshToken: '',
32+
expiresIn: 0,
33+
accessToken: '',
34+
id: '',
35+
name: '',
36+
picture: '',
37+
username: '',
38+
};
39+
}
40+
41+
async generateAuthUrl() {
42+
const state = makeId(32);
43+
const codeVerifier = randomBytes(64).toString('base64url');
44+
const challenge = Buffer.from(
45+
createHash('sha256').update(codeVerifier).digest()
46+
)
47+
.toString('base64')
48+
.replace(/=*$/g, '')
49+
.replace(/\+/g, '-')
50+
.replace(/\//g, '_');
51+
52+
return {
53+
url:
54+
'https://id.vk.com/authorize' +
55+
`?response_type=code` +
56+
`&client_id=${process.env.VK_ID}` +
57+
`&code_challenge_method=S256` +
58+
`&code_challenge=${challenge}` +
59+
`&redirect_uri=${encodeURIComponent(
60+
`${
61+
process?.env.FRONTEND_URL?.indexOf('https') == -1
62+
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
63+
: `${process?.env.FRONTEND_URL}`
64+
}/integrations/social/vk`
65+
)}` +
66+
`&state=${state}` +
67+
`&scope=${encodeURIComponent(this.scopes.join(' '))}`,
68+
codeVerifier,
69+
state,
70+
};
71+
}
72+
73+
async authenticate(params: {
74+
code: string;
75+
codeVerifier: string;
76+
refresh?: string;
77+
}) {
78+
const [code, device_id] = params.code.split('&&&&');
79+
80+
const formData = new FormData();
81+
formData.append('client_id', process.env.VK_ID!);
82+
formData.append('grant_type', 'authorization_code');
83+
formData.append('code_verifier', params.codeVerifier);
84+
formData.append('device_id', device_id);
85+
formData.append('code', code);
86+
formData.append(
87+
'redirect_uri',
88+
`${
89+
process?.env.FRONTEND_URL?.indexOf('https') == -1
90+
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
91+
: `${process?.env.FRONTEND_URL}`
92+
}/integrations/social/vk`
93+
);
94+
95+
const { access_token, scope, refresh_token } = await (
96+
await this.fetch('https://id.vk.com/oauth2/auth', {
97+
method: 'POST',
98+
body: formData,
99+
})
100+
).json();
101+
102+
const newFormData = new FormData();
103+
newFormData.append('client_id', process.env.VK_ID!);
104+
newFormData.append('access_token', access_token);
105+
106+
const {
107+
user: { user_id, first_name, last_name, avatar },
108+
} = await (
109+
await this.fetch('https://id.vk.com/oauth2/user_info', {
110+
method: 'POST',
111+
body: newFormData,
112+
})
113+
).json();
114+
115+
return {
116+
id: user_id,
117+
name: first_name + ' ' + last_name,
118+
accessToken: access_token,
119+
refreshToken: access_token,
120+
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
121+
picture: avatar,
122+
username: first_name.toLowerCase(),
123+
};
124+
}
125+
126+
async post(
127+
userId: string,
128+
accessToken: string,
129+
postDetails: PostDetails[]
130+
): Promise<PostResponse[]> {
131+
let replyTo = '';
132+
const values: PostResponse[] = [];
133+
134+
const uploading = await Promise.all(
135+
postDetails.map(async (post) => {
136+
return await Promise.all(
137+
(post?.media || []).map(async (media) => {
138+
const all = await (
139+
await this.fetch(
140+
media.url.indexOf('mp4') > -1
141+
? `https://api.vk.com/method/video.save?access_token=${accessToken}&v=5.251`
142+
: `https://api.vk.com/method/photos.getWallUploadServer?owner_id=${userId}&access_token=${accessToken}&v=5.251`
143+
)
144+
).json();
145+
146+
const { data } = await axios.get(media.url!, {
147+
responseType: 'stream',
148+
});
149+
150+
const slash = media.url.split('/').at(-1);
151+
152+
const formData = new FormDataNew();
153+
formData.append('photo', data, {
154+
filename: slash,
155+
contentType: mime.lookup(slash!) || '',
156+
});
157+
const value = (
158+
await axios.post(all.response.upload_url, formData, {
159+
headers: {
160+
...formData.getHeaders(),
161+
},
162+
})
163+
).data;
164+
165+
if (media.url.indexOf('mp4') > -1) {
166+
return {
167+
id: all.response.video_id,
168+
type: 'video',
169+
};
170+
}
171+
172+
const formSend = new FormData();
173+
formSend.append('photo', value.photo);
174+
formSend.append('server', value.server);
175+
formSend.append('hash', value.hash);
176+
177+
const { id } = (
178+
await (
179+
await fetch(
180+
`https://api.vk.com/method/photos.saveWallPhoto?access_token=${accessToken}&v=5.251`,
181+
{
182+
method: 'POST',
183+
body: formSend,
184+
}
185+
)
186+
).json()
187+
).response[0];
188+
189+
return {
190+
id,
191+
type: 'photo',
192+
};
193+
})
194+
);
195+
})
196+
);
197+
198+
let i = 0;
199+
for (const post of postDetails) {
200+
const list = (uploading?.[i] || []);
201+
202+
const body = new FormData();
203+
body.append('message', post.message);
204+
if (replyTo) {
205+
body.append('post_id', replyTo);
206+
}
207+
208+
if (list.length) {
209+
body.append(
210+
'attachments',
211+
list.map((p) => `${p.type}${userId}_${p.id}`).join(',')
212+
);
213+
}
214+
215+
const { response, ...all } = await (
216+
await this.fetch(
217+
`https://api.vk.com/method/${
218+
replyTo ? 'wall.createComment' : 'wall.post'
219+
}?v=5.251&access_token=${accessToken}&client_id=${process.env.VK_ID}`,
220+
{
221+
method: 'POST',
222+
body,
223+
}
224+
)
225+
).json();
226+
227+
228+
values.push({
229+
id: post.id,
230+
postId: String(response?.post_id || response?.comment_id),
231+
releaseURL: `https://vk.com/feed?w=wall${userId}_${
232+
response?.post_id || replyTo
233+
}`,
234+
status: 'completed',
235+
});
236+
237+
if (!replyTo) {
238+
replyTo = response.post_id;
239+
}
240+
241+
i++;
242+
}
243+
244+
return values;
245+
}
246+
}

0 commit comments

Comments
 (0)