22
loading...
This website collects cookies to deliver better user experience
https://api.telegram.org/bot[TOKEN]/getUpdates
where TOKEN
is the token given to you by the botfather. getUpdates
URL. To start a conversation with your bot, visit the URL https://t.me/[BOT_USERNAME]/start
where BOT_USERNAME
refers to the username for your Telegram bot. You will be directed to the Telegram app where you will see something similar to the screenshot below.getUpdates
URL, you'll see a new update.from
node, we can see the id of the message sender. Make a note of the displayed id as we'll use this later on. https://t.me/[BOT_USERNAME]
), you can identify users on your system and get their Telegram user ID. This can be done via the following steps:https://t.me/[BOT_USERNAME]?start=[UNIQUE_STRING]
) where UNIQUE_STRING
represents the string generated in step 1.getUpdates
URL and filter the results for a message update with text /start [UNIQUE_STRING]
. An implementation in Javascript is shown below.
const getBotUpdates = () =>
fetch(
"[https://api.telegram.org/bot{token}/getUpdates](https://api.telegram.org/bot%7Bbot_token%7D/getUpdates)"
).then((response) => response.json());
const getUserTelegramId = async (uniqueString) => {
const { result } = await getBotUpdates();
const messageUpdates = result.filter(
({ message }) => message?.text !== undefined
);
const userUpdate = messageUpdates.find(
({ message }) => message.text === `/start ${uniqueString}`
);
return userUpdate.message.from.id;
};
GET
request to https://api.telegram.org/bot[TOKEN]/sendMessage
with chat_id
and text
as the query parameters. Remember, TOKEN
is the token given to you by the botfather. Optionally, you can also include a third query parameter, parse_mode
that allows you to send HTML or Markdown content to the recipient.https://api.telegram.org/bot[TOKEN]/sendMessage?chat_id=[USER_ID]&text=Hello%20from%20your%20new%20bot
in your browser where token
is the token given to you by the botfather and USER_ID
is the id of the message sender you copied earlier. You will see a response similar to the screenshot below.npx create-next-app telegram-react
studio
and initialise the project using the sanity init
command.cd telegram-react
mkdir studio
sanity init
? Select project to use: Create new project
? Your project name: telegram-react
? Select dataset to use: production
? Project output path: telegram-react/studio
? Select project template: Movie project (schema + sample data)
? Add a sampling of sci-fi movies to your dataset on the hosted backend? Yes
npm i next-sanity
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
npm i --save-dev @sanity/cli concurrently
scripts
node in package.json
to match the following."scripts": {
"dev": "concurrently \"next\" \"cd studio && sanity start\"",
"build": "echo 'Building Sanity to public/studio…' && cd studio && sanity build ../public/studio -y && cd .. && next build",
"start": "next start",
"lint": "next lint"
},
pages/_app.js
file and inject the ChakraProvider
. Open pages/_app.js
(you can create the file if it doesn't exist) and add the following.import '../styles/globals.css'
import {ChakraProvider, Container} from "@chakra-ui/react";
function MyApp({Component, pageProps}) {
return (
<ChakraProvider>
<Container maxW='container.xl' centerContent>
<Component {...pageProps} />
</Container>
</ChakraProvider>
);
}
export default MyApp
.env.local
and add the following to the env file.# Telegram
TELEGRAM_BOT_TOKEN = "your_telegram_bot_token"
TELEGRAM_USER_ID = "your_telegram_user_id"
# For Next Locally
NEXT_PUBLIC_SANITY_PROJECT_ID = "your_project_id"
NEXT_PUBLIC_SANITY_DATASET = "production"
utility
. api.js
in the utility
directory. This file will contain a helper function for making API requests. In this file add the following code.export const makePostRequest = (url, details) => {
return fetch(url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(details),
})
.then((response) => response.json());
};
makePostRequest
which takes the endpoint URL and the values of the POST request and uses Fetch to send a POST request accordingly. The function returns a promise which resolves to the JSON content of the API response. telegram.js
in the utility
directory. This file will contain a helper function that will help us to interact with the Telegram Bot API easily. In this file add the following code.import {makePostRequest} from "./api";
const telegramBotKey = process.env.TELEGRAM_BOT_TOKEN;
const chat_id = process.env.TELEGRAM_USER_ID;
export const sendNotification = async (text, parse_mode) => {
const endpoint = `https://api.telegram.org/bot${telegramBotKey}/sendMessage`;
await makePostRequest(endpoint,
{
text,
parse_mode,
chat_id
});
};
sendNotification
function takes the text of the notification and the mode which Telegram should use in parsing it. For this guide, we will use HTML formatted text. You can read more about formatting options for Telegram messages here. objectFormat.js
in the utility
directory. This file will contain a helper function that we can use to generate the appropriate HTML formatted text for Telegram requests based on the provided object. In this file, add the following code.const capitalize = (input) =>
`${input.charAt(0).toUpperCase()}${input.slice(1)}`;
const capitalizeKey = (input) =>
input.split(/(?=[A-Z])/).map(word => capitalize(word)).join(' ');
const newLineToken = '\n';
const lineBreak = newLineToken.repeat(2);
const formatTitle = title => `<b><i>${title}</i></b>`;
const formatArray = childArray =>
childArray.map((child, index) => `${index + 1}. ${child}`).join(newLineToken);
const formatEntry = ([key, value]) => {
const formattedTitle = formatTitle(capitalizeKey(key));
const formattedBody = Array.isArray(value) ? formatArray(value) : value;
return `${formattedTitle}${newLineToken}${formattedBody}`;
};
export const toHtmlFormat = (title, body) => {
const formattedTitle = formatTitle(title);
const formattedBody = Object.entries(body).map(formatEntry).join(lineBreak);
return `${formattedTitle}${lineBreak}${formattedBody}`;
};
toHtmlFormat
function takes the title (the content of the first line of the formatted text) and the body (which is an object). It maps through the object, capitalises the object keys, and then returns the key formatted as a title and the value of the object underneath. formatArray
function is used to iterate through each member of the array and format each child. This function is naive in the sense that it assumes that the array provided is comprised only of strings.dateFormat.js
in the utility
directory. This file will contain a helper function that will allow us to present dates in a human-readable format. In it add the following code.export const formatDate = date =>
new Date(date).toLocaleString('en-GB', {
month: 'long',
weekday: 'long',
day: 'numeric',
year: 'numeric'
});
sanity.js
in the utility
directory. In it, add the following code.import {createClient, createImageUrlBuilder, createPortableTextComponent,} from "next-sanity";
const config = {
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
useCdn: process.env.NODE_ENV === "production",
apiVersion: '2021-10-12'
};
if (!config.projectId) {
throw Error("The Project ID is not set. Check your environment variables.");
}
if (!config.dataset) {
throw Error("The dataset name is not set. Check your environment variables.");
}
export const urlFor = (source) => createImageUrlBuilder(config).image(source);
export const PortableText = createPortableTextComponent({
...config,
serializers: {},
});
export const sanityClient = createClient(config);
urlFor
helper function will be used for generating image URLs with only the asset reference data in your documents.PortableText
constant returns a component that we can use to render block content from Sanity Studio. In this guide, we'll be using it to render the overview of a movie. sanityClient
is used for fetching data in the getProps
page functions.components
directory, create a new file called MovieCard.js
. When we display the movies in a grid, each movie will be rendered with the MovieCard
component. In the components/MovieCard.js
file, add the following.import React from "react";
import {Box, Image} from "@chakra-ui/react";
import {urlFor} from "../utility/sanity";
import Link from 'next/link';
import {formatDate} from "../utility/dateFormat";
const MovieCard = ({_id, title, poster, releaseDate, slug}) => {
return (
<Link href={`/movies/${slug.current}`}>
<a>
<Box
maxW="sm"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
>
<Image
src={urlFor(poster).width(300).height(300)}
alt={`${title} movie poster`}
/>
<Box p='6'>
<Box
mt="1"
fontWeight="semibold"
as="h3"
isTruncated
>
{title}
</Box>
<Box
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
>
Released on {formatDate(releaseDate)}
</Box>
</Box>
</Box>
</a>
</Link>
);
};
export default MovieCard;
urlFor
helper function we created earlier), the title, and the release date (formatted using the formatDate
helper function we created earlier). components
directory, create a new file called MoviesPage.js
and add the following code to it.import React from "react";
import {SimpleGrid} from "@chakra-ui/react";
import MovieCard from "./MovieCard";
const MoviesPage = ({movies}) => {
return (
<SimpleGrid columns={4} spacing={20} padding='10'>
{movies.map(movie => (
<MovieCard key={movie._id} {...movie}/>
))}
</SimpleGrid>
);
};
export default MoviesPage;
MovieCard
component we created earlier. components
directory, create a new file called Team.js
. This component will be used to render teams (either the crew members or the cast of a movie). Add the following code to the component.import React from "react";
import {urlFor} from "../utility/sanity";
import {Box, Image, SimpleGrid} from "@chakra-ui/react";
const Team = ({members, isCast}) => {
const defaultImageUrl = "https://bit.ly/3aUzAKZ";
return (
<SimpleGrid columns={3} spacing={10} padding='5'>
{members.map(member => {
const {name, _key, image} = member.person;
const imageUrl = image ? urlFor(image).width(50).height(50) :
defaultImageUrl;
const extra = isCast ? member.characterName : member.job;
return (
<Box key={_key}>
<Image
src={imageUrl}
alt={`${name}`}
w={50}
h={50}
/>
<Box key={1}>{name}</Box>
<Box
key={2}
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
>
{extra}
</Box>
</Box>
)
})}
</SimpleGrid>
);
};
export default Team;
Team
component takes two props - the members of the team and a boolean variable named isCast
which represents whether or not the team to be rendered is the cast of the team. components
directory, create a new file called MoviePage.js
and add the following code to it. This should not be confused with the MoviesPage.js
component we built earlier. This component is rendered when a movie card is clicked. It takes a movie as a prop and renders the movie details. It also adds a button which when clicked, shares the movie details via Telegram.import React, {useState} from "react";
import {
Alert,
AlertIcon,
Box,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Button,
Center,
CloseButton,
Heading,
Image,
Stack
} from "@chakra-ui/react";
import {PortableText, urlFor} from "../utility/sanity";
import Link from 'next/link';
import Team from "./Team";
import {toHtmlFormat} from "../utility/objectFormat";
import {formatDate} from "../utility/dateFormat";
import {makePostRequest} from "../utility/api";
const MoviePage = ({movie}) => {
const [shouldShowAlert, setShouldShowAlert] = useState(false);
const [alertContent, setAlertContent] = useState(null);
const hideAlert = () => {
setAlertContent(null);
setShouldShowAlert(false);
};
const formattedReleaseDate = formatDate(movie.releaseDate);
const getDetailsFromCastMember = ({characterName, person}) =>
`${person.name} as ${characterName}`;
const getDetailsFromCrewMember = ({job, person}) =>
`${person.name} - ${job}`;
const sendMovieInfoToUser = async () => {
const {cast, crew, popularity, overview, title} = movie;
const detailsToSend = {
releaseDate: formattedReleaseDate,
cast: cast.map(getDetailsFromCastMember),
crew: crew.map(getDetailsFromCrewMember),
popularity,
summary: overview[0].children[0].text
};
const response = await makePostRequest("/api/share", {
text: toHtmlFormat(title, detailsToSend),
parseMode: 'html'
});
setAlertContent(response.message);
setShouldShowAlert(true);
setTimeout(hideAlert, 3000);
}
return (
<Stack spacing={3} padding='10'>
{shouldShowAlert && (
<Alert status="success">
<AlertIcon/>
{alertContent}
<CloseButton
position="absolute"
right="8px"
top="8px"
onClick={hideAlert}
/>
</Alert>
)}
<Box maxW="3xl" borderRadius="lg" overflow="hidden">
<Breadcrumb>
<BreadcrumbItem>
<Link href="/">
<BreadcrumbLink>Movies</BreadcrumbLink>
</Link>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<Link
href={`/movies/${movie.slug.current}`}
>
<BreadcrumbLink>{movie.title}</BreadcrumbLink>
</Link>
</BreadcrumbItem>
</Breadcrumb>
<Center>
<Heading as='h1'>{movie.title}</Heading>
</Center>
<Center>
<Box p='6'>
<Image
src={urlFor(movie.poster).width(300).height(300)}
alt={`${movie.title} movie poster`}
/>
<Center mt={2}>
<Box>
<Box
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
>
Released on {formattedReleaseDate}
</Box>
<Box
color="gray.500"
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
>
Popularity: {movie.popularity}
</Box>
<Center>
<Button
colorScheme="teal"
variant="outline"
mt='1'
onClick={sendMovieInfoToUser}
>
Share details via Telegram
</Button>
</Center>
</Box>
</Center>
</Box>
</Center>
<Box>
<Heading as='h3' size='lg'>Overview</Heading>
<PortableText blocks={movie.overview}/>
</Box>
<Box pt='10'>
<Heading as='h3' size='lg'>Cast</Heading>
<Team members={movie.cast} isCast={true}/>
</Box>
<Box pt='10'>
<Heading as='h3' size='lg'>Crew</Heading>
<Team members={movie.crew} isCast={false}/>
</Box>
</Box>
</Stack>
);
};
export default MoviePage;
MoviePage
component, we make a request to api/share
to send the movie details, let's create a handler for this endpoint. We can do this in our current project as Next.js provides a solution for us to build APIs with it.pages/api
folder, create a new file called share.js
. In it, add the following code.import {sendNotification} from "../../utility/telegram";
const handler = async (request, response) => {
switch (request.method) {
case "POST":
const {text, parseMode} = request.body;
await sendNotification(text, parseMode);
response.status(200)
.json({message: 'Details shared successfully'});
break;
default:
response.status(405)
.end("This method is not allowed for this route.");
}
};
export default handler;
sendNotification
helper function we declared earlier, we send the details to our Telegram bot and eventually us!!pages/index.js
file to match the following.import {sanityClient} from "../utility/sanity";
import MoviesPage from "../components/MoviesPage";
import {groq} from "next-sanity";
const query = groq`*[_type == "movie" && defined(slug.current)]`;
const Home = ({movies}) => {
return (
<MoviesPage movies={movies}/>
)
}
export const getStaticProps = async () => {
const movies = await sanityClient.fetch(query);
return {
props: {
movies,
},
};
}
export default Home;
getStaticProps
we fetch all the movies with a defined slug from the backend, this is passed as a prop the MoviesPage
component we created earlier. pages
directory, create a new folder called movies
. In the movies
folder, create a new file called [slug].js
. By taking advantage of Next.JS's file-based router system, this folder and file naming structure will allow us requests to the URL /movies/{slug}
where slug
corresponds to the slug of the selected movie. Add the following code to pages/movies/[slug].js
.import {sanityClient} from "../../utility/sanity";
import {groq} from "next-sanity";
import {useRouter} from "next/router";
import Error from "next/error";
import MoviePage from "../../components/MoviePage";
const query = groq`*[_type == "movie" && slug.current == $slug][0]{
releaseDate, overview, popularity, poster, title, slug,
'crew': crewMembers[]{_key, department, job, person->{_id, name, image}},
'cast': castMembers[]{_key, characterName, person->{_id, name, image}}
}`;
const MoviePageContainer = ({movie}) => {
const router = useRouter();
if (!(router.isFallback || movie?.slug)) {
return <Error statusCode={404}/>;
}
return <MoviePage movie={movie}/>;
};
export const getStaticProps = async ({params}) => {
const movie = await sanityClient.fetch(query, {
slug: params.slug,
});
return {
props: {movie},
};
}
export const getStaticPaths = async () => {
const paths = await sanityClient.fetch(
`*[_type == "movie" && defined(slug.current)][].slug.current`
);
return {
paths: paths.map((slug) => ({params: {slug}})),
fallback: true,
};
}
export default MoviePageContainer;
npm run dev