feat: telegram channel
This commit is contained in:
parent
339416f80f
commit
0dde88243b
|
|
@ -36,6 +36,7 @@ import {
|
|||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -609,4 +610,11 @@ export class IntegrationsController {
|
|||
) {
|
||||
return this._integrationService.changePlugActivation(org.id, id, status);
|
||||
}
|
||||
|
||||
@Get('/telegram/updates')
|
||||
async getUpdates(
|
||||
@Query() query: { word: string; id?: number },
|
||||
) {
|
||||
return new TelegramProvider().getBotId(query);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -42,6 +42,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
|
||||
telegramBotName={process.env.TELEGRAM_BOT_NAME!}
|
||||
neynarClientId={process.env.NEYNAR_CLIENT_ID!}
|
||||
>
|
||||
<ToltScript />
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@ export const AddEditModal: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={selectedIntegrations?.[0]?.picture}
|
||||
src={selectedIntegrations?.[0]?.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={selectedIntegrations?.[0]?.identifier}
|
||||
width={32}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const BotPicture: FC<{
|
|||
const modal = useModals();
|
||||
const toast = useToaster();
|
||||
const [nick, setNickname] = useState(props.integration.name);
|
||||
const [picture, setPicture] = useState(props.integration.picture);
|
||||
const [picture, setPicture] = useState(props.integration.picture || '/no-picture.jpg');
|
||||
|
||||
const fetch = useFetch();
|
||||
const submitForm: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
|
|
|
|||
|
|
@ -634,7 +634,7 @@ export const CalendarColumn: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={selectedIntegrations.picture}
|
||||
src={selectedIntegrations.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={selectedIntegrations.identifier}
|
||||
width={32}
|
||||
|
|
@ -747,7 +747,7 @@ const CalendarItem: FC<{
|
|||
>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full"
|
||||
src={post.integration.picture!}
|
||||
src={post.integration.picture! || '/no-picture.jpg'}
|
||||
/>
|
||||
<img
|
||||
className="w-[12px] h-[12px] rounded-full absolute z-10 top-[10px] right-0 border border-fifth"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
src={integration?.picture || '/no-picture.jpg'}
|
||||
alt="x"
|
||||
className="rounded-full relative z-[2]"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export const PickPlatforms: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={integration.picture}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
|
|
@ -302,7 +302,7 @@ export const PickPlatforms: FC<{
|
|||
<div className="flex items-center justify-center gap-[10px]">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={integration.picture}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={24}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ export const MenuComponent: FC<
|
|||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture!}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import MastodonProvider from '@gitroom/frontend/components/launches/providers/ma
|
|||
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
|
||||
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
|
||||
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
|
||||
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -42,6 +43,7 @@ export const Providers = [
|
|||
{identifier: 'bluesky', component: BlueskyProvider},
|
||||
{identifier: 'lemmy', component: LemmyProvider},
|
||||
{identifier: 'wrapcast', component: WarpcastProvider},
|
||||
{identifier: 'telegram', component: TelegramProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
'use client';
|
||||
import '@neynar/react/dist/style.css';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
export const TelegramProvider: FC<Web3ProviderInterface> = (props) => {
|
||||
const { onComplete, nonce } = props;
|
||||
const {telegramBotName} = useVariables();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const word = useRef(makeId(4));
|
||||
const stop = useRef(false);
|
||||
const [step, setStep] = useState(false);
|
||||
const toaster = useToaster();
|
||||
|
||||
async function* load() {
|
||||
let id = '';
|
||||
while (true) {
|
||||
const data = await (
|
||||
await fetch(
|
||||
`/integrations/telegram/updates?word=${word.current}${
|
||||
id ? `&id=${id}` : ''
|
||||
}`
|
||||
)
|
||||
).json();
|
||||
|
||||
if (data.lastChatId) {
|
||||
id = data.lastChatId;
|
||||
}
|
||||
|
||||
yield data;
|
||||
}
|
||||
}
|
||||
|
||||
const loadAll = async () => {
|
||||
stop.current = false;
|
||||
setStep(true);
|
||||
const generator = load();
|
||||
for await (const data of generator) {
|
||||
if (stop.current) {
|
||||
return;
|
||||
}
|
||||
if (data.chatId) {
|
||||
onComplete(data.chatId, nonce);
|
||||
return;
|
||||
}
|
||||
await timer(2000);
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = useCallback(() => {
|
||||
copy(`/connect ${word.current}`);
|
||||
toaster.show('Copied to clipboard', 'success');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stop.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-[700px]">
|
||||
<TopTitle title={`Add Telegram`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="justify-center items-center flex flex-col pt-[16px]">
|
||||
<div>
|
||||
Please add <strong>@{telegramBotName}</strong> to your
|
||||
telegram group / channel and click here:
|
||||
</div>
|
||||
{!step ? (
|
||||
<div className="w-full mt-[16px]" onClick={loadAll}>
|
||||
<div
|
||||
className={`cursor-pointer bg-[#2EA6DD] h-[44px] rounded-[4px] flex justify-center items-center text-white ${interClass} gap-[4px]`}
|
||||
>
|
||||
<svg
|
||||
width="51"
|
||||
height="22"
|
||||
viewBox="0 0 72 63"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M71.85 3.00001L60.612 60.378C60.612 60.378 60.129 63 56.877 63C55.149 63 54.258 62.178 54.258 62.178L29.916 41.979L18.006 35.976L2.721 31.911C2.721 31.911 0 31.125 0 28.875C0 27 2.799 26.106 2.799 26.106L66.747 0.70201C66.747 0.70201 68.7 -0.00299041 70.125 9.58803e-06C71.001 9.58803e-06 72 0.37501 72 1.50001C72 2.25001 71.85 3.00001 71.85 3.00001Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M39.0005 49.5147L28.7225 59.6367C28.7225 59.6367 28.2755 59.9817 27.6785 59.9967C27.4715 60.0027 27.2495 59.9697 27.0215 59.8677L29.9135 41.9727L39.0005 49.5147Z"
|
||||
fill="#B0BEC5"
|
||||
/>
|
||||
<path
|
||||
d="M59.691 12.5877C59.184 11.9277 58.248 11.8077 57.588 12.3087L18 35.9997C18 35.9997 24.318 53.6757 25.281 56.7357C26.247 59.7987 27.021 59.8707 27.021 59.8707L29.913 41.9757L59.409 14.6877C60.069 14.1867 60.192 13.2477 59.691 12.5877Z"
|
||||
fill="#CFD8DC"
|
||||
/>
|
||||
</svg>
|
||||
<div>Connect Telegram</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full text-center" onClick={copyText}>
|
||||
Please add the following command in your chat:
|
||||
<div className="mt-[16px] flex">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label=""
|
||||
value={`/connect ${word.current}`}
|
||||
name=""
|
||||
disableForm={true}
|
||||
/>
|
||||
</div>
|
||||
<Button>Copy</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import { FC } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { WrapcasterProvider } from '@gitroom/frontend/components/launches/web3/providers/wrapcaster.provider';
|
||||
import { TelegramProvider } from '@gitroom/frontend/components/launches/web3/providers/telegram.provider';
|
||||
|
||||
export const web3List: {
|
||||
identifier: string;
|
||||
component: FC<Web3ProviderInterface>;
|
||||
}[] = [
|
||||
{
|
||||
identifier: 'telegram',
|
||||
component: TelegramProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'wrapcast',
|
||||
component: WrapcasterProvider,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { BlueskyProvider } from '@gitroom/nestjs-libraries/integrations/social/b
|
|||
import { LemmyProvider } from '@gitroom/nestjs-libraries/integrations/social/lemmy.provider';
|
||||
import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.standalone.provider';
|
||||
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
|
||||
const socialIntegrationList: SocialProvider[] = [
|
||||
new XProvider(),
|
||||
|
|
@ -44,6 +45,7 @@ const socialIntegrationList: SocialProvider[] = [
|
|||
new BlueskyProvider(),
|
||||
new LemmyProvider(),
|
||||
new FarcasterProvider(),
|
||||
new TelegramProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
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 TelegramBot from 'node-telegram-bot-api';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
const telegramBot = new TelegramBot(process.env.TELEGRAM_TOKEN!);
|
||||
|
||||
export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'telegram';
|
||||
name = 'Telegram';
|
||||
isBetweenSteps = false;
|
||||
isWeb3 = true;
|
||||
scopes = [];
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(17);
|
||||
return {
|
||||
url: state,
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const chat = await telegramBot.getChat(params.code);
|
||||
|
||||
console.log(JSON.stringify(chat))
|
||||
if (!chat?.id) {
|
||||
return 'No chat found';
|
||||
}
|
||||
|
||||
const photo = !chat?.photo?.big_file_id
|
||||
? ''
|
||||
: await telegramBot.getFileLink(chat.photo.big_file_id);
|
||||
return {
|
||||
id: String(chat.username),
|
||||
name: chat.title!,
|
||||
accessToken: String(chat.id),
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: photo,
|
||||
username: chat.username!,
|
||||
};
|
||||
}
|
||||
|
||||
async getBotId(query: { id?: number; word: string }) {
|
||||
const res = await telegramBot.getUpdates({
|
||||
...(query.id ? { offset: query.id } : {}),
|
||||
});
|
||||
|
||||
const chatId = res?.find(
|
||||
(p) => p?.message?.text === `/connect ${query.word}`
|
||||
)?.message?.chat?.id;
|
||||
|
||||
return chatId
|
||||
? {
|
||||
chatId,
|
||||
}
|
||||
: res.length > 0
|
||||
? {
|
||||
lastChatId: res?.[res.length - 1]?.message?.chat?.id,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const ids: PostResponse[] = [];
|
||||
for (const message of postDetails) {
|
||||
if (
|
||||
(message?.media?.length || 0) < 3 &&
|
||||
(message?.media?.length || 0) > 0
|
||||
) {
|
||||
const [{ message_id }] = await telegramBot.sendMediaGroup(
|
||||
accessToken,
|
||||
message?.media?.map((m) => ({
|
||||
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
|
||||
caption: message.message,
|
||||
media: m.url,
|
||||
})) || []
|
||||
);
|
||||
|
||||
ids.push({
|
||||
id: message.id,
|
||||
postId: String(message_id),
|
||||
releaseURL: `https://t.me/${id}/${message_id}`,
|
||||
status: 'completed',
|
||||
});
|
||||
} else {
|
||||
const { message_id } = await telegramBot.sendMessage(
|
||||
accessToken,
|
||||
message.message
|
||||
);
|
||||
|
||||
ids.push({
|
||||
id: message.id,
|
||||
postId: String(message_id),
|
||||
releaseURL: `https://t.me/${id}/${message_id}`,
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
if ((message?.media?.length || 0) > 0) {
|
||||
await telegramBot.sendMediaGroup(
|
||||
accessToken,
|
||||
message?.media?.map((m) => ({
|
||||
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
|
||||
media: m.url,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ interface VariableContextInterface {
|
|||
discordUrl: string;
|
||||
uploadDirectory: string;
|
||||
facebookPixel: string;
|
||||
telegramBotName: string;
|
||||
neynarClientId: string;
|
||||
tolt: string;
|
||||
}
|
||||
|
|
@ -24,6 +25,7 @@ const VariableContext = createContext({
|
|||
backendUrl: '',
|
||||
discordUrl: '',
|
||||
uploadDirectory: '',
|
||||
telegramBotName: '',
|
||||
facebookPixel: '',
|
||||
neynarClientId: '',
|
||||
tolt: '',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -133,6 +133,7 @@
|
|||
"nestjs-real-ip": "^3.0.1",
|
||||
"next": "^14.2.14",
|
||||
"next-plausible": "^3.12.0",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"nx": "19.7.2",
|
||||
"openai": "^4.47.1",
|
||||
|
|
@ -187,6 +188,7 @@
|
|||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/node-telegram-bot-api": "^0.64.7",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
|
|
|||
Loading…
Reference in New Issue