feat: vk provider

This commit is contained in:
Nevo David 2025-04-18 19:45:45 +07:00
parent 7c265e5f67
commit 0abd68961f
6 changed files with 269 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -22,6 +22,14 @@ export default async function Page({
};
}
if (provider === 'vk') {
searchParams = {
...searchParams,
state: searchParams.state || '',
code: searchParams.code + '&&&&' + searchParams.device_id
};
}
const data = await internalFetch(`/integrations/social/${provider}/connect`, {
method: 'POST',
body: JSON.stringify(searchParams),

View File

@ -21,6 +21,7 @@ import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
import VkProvider from '@gitroom/frontend/components/launches/providers/vk/vk.provider';
export const Providers = [
{identifier: 'devto', component: DevtoProvider},
@ -46,6 +47,7 @@ export const Providers = [
{identifier: 'wrapcast', component: WarpcastProvider},
{identifier: 'telegram', component: TelegramProvider},
{identifier: 'nostr', component: NostrProvider},
{identifier: 'vk', component: VkProvider},
];

View File

@ -0,0 +1,11 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
export default withProvider(
null,
undefined,
undefined,
async (posts) => {
return true;
},
2048
);

View File

@ -26,6 +26,7 @@ import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrati
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
import { VkProvider } from '@gitroom/nestjs-libraries/integrations/social/vk.provider';
export const socialIntegrationList: SocialProvider[] = [
new XProvider(),
@ -48,6 +49,7 @@ export const socialIntegrationList: SocialProvider[] = [
new FarcasterProvider(),
new TelegramProvider(),
new NostrProvider(),
new VkProvider(),
// new MastodonCustomProvider(),
];

View File

@ -0,0 +1,246 @@
import {
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import dayjs from 'dayjs';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { createHash, randomBytes } from 'crypto';
import axios from 'axios';
import FormDataNew from 'form-data';
import mime from 'mime-types';
export class VkProvider extends SocialAbstract implements SocialProvider {
identifier = 'vk';
name = 'VK';
isBetweenSteps = false;
scopes = [
'vkid.personal_info',
'email',
'wall',
'status',
'docs',
'photos',
'video',
];
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
return {
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl() {
const state = makeId(32);
const codeVerifier = randomBytes(64).toString('base64url');
const challenge = Buffer.from(
createHash('sha256').update(codeVerifier).digest()
)
.toString('base64')
.replace(/=*$/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return {
url:
'https://id.vk.com/authorize' +
`?response_type=code` +
`&client_id=${process.env.VK_ID}` +
`&code_challenge_method=S256` +
`&code_challenge=${challenge}` +
`&redirect_uri=${encodeURIComponent(
`${
process?.env.FRONTEND_URL?.indexOf('https') == -1
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
: `${process?.env.FRONTEND_URL}`
}/integrations/social/vk`
)}` +
`&state=${state}` +
`&scope=${encodeURIComponent(this.scopes.join(' '))}`,
codeVerifier,
state,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const [code, device_id] = params.code.split('&&&&');
const formData = new FormData();
formData.append('client_id', process.env.VK_ID!);
formData.append('grant_type', 'authorization_code');
formData.append('code_verifier', params.codeVerifier);
formData.append('device_id', device_id);
formData.append('code', code);
formData.append(
'redirect_uri',
`${
process?.env.FRONTEND_URL?.indexOf('https') == -1
? `https://redirectmeto.com/${process?.env.FRONTEND_URL}`
: `${process?.env.FRONTEND_URL}`
}/integrations/social/vk`
);
const { access_token, scope, refresh_token } = await (
await this.fetch('https://id.vk.com/oauth2/auth', {
method: 'POST',
body: formData,
})
).json();
const newFormData = new FormData();
newFormData.append('client_id', process.env.VK_ID!);
newFormData.append('access_token', access_token);
const {
user: { user_id, first_name, last_name, avatar },
} = await (
await this.fetch('https://id.vk.com/oauth2/user_info', {
method: 'POST',
body: newFormData,
})
).json();
return {
id: user_id,
name: first_name + ' ' + last_name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: avatar,
username: first_name.toLowerCase(),
};
}
async post(
userId: string,
accessToken: string,
postDetails: PostDetails[]
): Promise<PostResponse[]> {
let replyTo = '';
const values: PostResponse[] = [];
const uploading = await Promise.all(
postDetails.map(async (post) => {
return await Promise.all(
(post?.media || []).map(async (media) => {
const all = await (
await this.fetch(
media.url.indexOf('mp4') > -1
? `https://api.vk.com/method/video.save?access_token=${accessToken}&v=5.251`
: `https://api.vk.com/method/photos.getWallUploadServer?owner_id=${userId}&access_token=${accessToken}&v=5.251`
)
).json();
const { data } = await axios.get(media.url!, {
responseType: 'stream',
});
const slash = media.url.split('/').at(-1);
const formData = new FormDataNew();
formData.append('photo', data, {
filename: slash,
contentType: mime.lookup(slash!) || '',
});
const value = (
await axios.post(all.response.upload_url, formData, {
headers: {
...formData.getHeaders(),
},
})
).data;
if (media.url.indexOf('mp4') > -1) {
return {
id: all.response.video_id,
type: 'video',
};
}
const formSend = new FormData();
formSend.append('photo', value.photo);
formSend.append('server', value.server);
formSend.append('hash', value.hash);
const { id } = (
await (
await fetch(
`https://api.vk.com/method/photos.saveWallPhoto?access_token=${accessToken}&v=5.251`,
{
method: 'POST',
body: formSend,
}
)
).json()
).response[0];
return {
id,
type: 'photo',
};
})
);
})
);
let i = 0;
for (const post of postDetails) {
const list = (uploading?.[i] || []);
const body = new FormData();
body.append('message', post.message);
if (replyTo) {
body.append('post_id', replyTo);
}
if (list.length) {
body.append(
'attachments',
list.map((p) => `${p.type}${userId}_${p.id}`).join(',')
);
}
const { response, ...all } = await (
await this.fetch(
`https://api.vk.com/method/${
replyTo ? 'wall.createComment' : 'wall.post'
}?v=5.251&access_token=${accessToken}&client_id=${process.env.VK_ID}`,
{
method: 'POST',
body,
}
)
).json();
values.push({
id: post.id,
postId: String(response?.post_id || response?.comment_id),
releaseURL: `https://vk.com/feed?w=wall${userId}_${
response?.post_id || replyTo
}`,
status: 'completed',
});
if (!replyTo) {
replyTo = response.post_id;
}
i++;
}
return values;
}
}