feat: telegram channel

This commit is contained in:
Nevo David 2025-01-20 20:09:19 +07:00
parent 339416f80f
commit 0dde88243b
18 changed files with 1061 additions and 2521 deletions

View File

@ -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

View File

@ -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 />

View File

@ -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}

View File

@ -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(

View File

@ -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"

View File

@ -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]"
/>

View File

@ -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}

View File

@ -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}

View File

@ -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},
];

View File

@ -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
);

View File

@ -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&nbsp;<strong>@{telegramBotName}</strong>&nbsp;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>
);
};

View File

@ -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,

View File

@ -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(),
];

View File

@ -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;
}
}

View File

@ -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: '',

3243
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",