Skip to content

Commit ba3187e

Browse files
committed
add: weather bot
0 parents  commit ba3187e

File tree

9 files changed

+432
-0
lines changed

9 files changed

+432
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
env.sh

.vscode/settings.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"deno.enable": true,
3+
"deno.unstable": true,
4+
"editor.formatOnSave": true,
5+
}

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Home for all my telegram bots created using Deno.

weather/bot.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
Bot,
3+
Context,
4+
InlineKeyboard,
5+
NextFunction,
6+
} from "https://deno.land/x/[email protected]/mod.ts";
7+
8+
import { escape } from "./formatter.ts";
9+
import { forecast, weather, astronomy, api } from "./handler.ts";
10+
11+
const BOT_TOKEN = Deno.env.get("BOT_TOKEN");
12+
if (!BOT_TOKEN) throw new Error("Bot Token is not set!");
13+
14+
const bot = new Bot(BOT_TOKEN);
15+
16+
async function responseTime(ctx: Context, next: NextFunction): Promise<void> {
17+
if (!ctx.inlineQuery?.query) return await next();
18+
console.log(ctx.inlineQuery?.query);
19+
const before = Date.now(); // milliseconds
20+
await next();
21+
const after = Date.now(); // milliseconds
22+
console.log(`Response time: ${after - before} ms`);
23+
}
24+
25+
bot.use(responseTime);
26+
27+
bot.inlineQuery(/^[\w\s'-]+$/, async (ctx) => {
28+
const query = ctx.inlineQuery.query?.trim();
29+
if (!query) return;
30+
const locations = await api("search", query);
31+
if (!locations || !locations.length) return;
32+
return ctx.answerInlineQuery(
33+
locations.map((location) => ({
34+
type: "article",
35+
id: `id:${location.id}`,
36+
title: escape(`${location.name}`),
37+
description: escape(`${location.region}, ${location.country}`),
38+
input_message_content: {
39+
message_text: "Crunching weather data for you...",
40+
},
41+
reply_markup: new InlineKeyboard().switchInlineCurrent(
42+
"Try another location",
43+
query.slice(0, -1)
44+
),
45+
})),
46+
{
47+
cache_time: 0,
48+
}
49+
);
50+
});
51+
52+
bot.on("chosen_inline_result", (ctx) => {
53+
const location = ctx.chosenInlineResult.result_id;
54+
const messageId = ctx.inlineMessageId;
55+
if (!messageId) return;
56+
return weather(ctx, location, messageId, ctx.from.id);
57+
});
58+
59+
bot.on("callback_query:data", async (ctx) => {
60+
const messageId = ctx.inlineMessageId;
61+
if (!messageId) return;
62+
const data = JSON.parse(ctx.callbackQuery.data) as {
63+
t: string;
64+
lc: string;
65+
uid?: number;
66+
};
67+
if (ctx.callbackQuery?.from?.id !== data.uid) {
68+
return ctx.answerCallbackQuery(
69+
"Only the person who sent the above message can use this button."
70+
);
71+
}
72+
if (data.t === "astronomy") {
73+
return await astronomy(ctx, data.lc, messageId, data.uid);
74+
}
75+
if (data.t === "weather") {
76+
return await weather(ctx, data.lc, messageId, data.uid);
77+
}
78+
if (data.t === "forecast") {
79+
return await forecast(ctx, data.lc, messageId, data.uid);
80+
}
81+
return ctx.api.editMessageTextInline(messageId, "Something went wrong!", {
82+
parse_mode: "HTML",
83+
reply_markup: new InlineKeyboard()
84+
.text(
85+
"Astronomy",
86+
`${JSON.stringify({ t: "astronomy", lc: data.lc, uid: data.uid })}`
87+
)
88+
.text(
89+
"Weather",
90+
`${JSON.stringify({ t: "weather", lc: data.lc, uid: data.uid })}`
91+
)
92+
.text(
93+
"Forecast",
94+
`${JSON.stringify({ t: "forecast", lc: data.lc, uid: data.uid })}`
95+
),
96+
});
97+
});
98+
export { bot };

weather/dev.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { bot } from "./bot.ts";
2+
3+
bot.catch(console.error);
4+
5+
bot.start({
6+
drop_pending_updates: true,
7+
allowed_updates: ["chosen_inline_result", "inline_query", "callback_query"],
8+
onStart: () => console.log("Bot started"),
9+
});

weather/formatter.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Astronomy, Forecast, Weather } from "./type.ts";
2+
3+
export function escape(text: string) {
4+
return text
5+
.replaceAll("<", "&lt;")
6+
.replaceAll(">", "&gt;")
7+
.replaceAll("&", "&amp;");
8+
}
9+
10+
export function formatCurrentWeather(weather: Weather) {
11+
const weatherTime = weather.current.last_updated_epoch;
12+
const localTime = weather.location.localtime_epoch;
13+
return (
14+
`<b>🌡️ ${escape(
15+
`${weather.location.name}, ${weather.location.region}, ${weather.location.country}`
16+
)}</b>\n\n` +
17+
`<b>Time</b>: <code>${weather.location.localtime}</code>\n\n` +
18+
`<b>Temperature</b>: <code>${weather.current.temp_c}°C</code>\n` +
19+
`<b>Humidity</b>: <code>${weather.current.humidity}%</code>\n` +
20+
`<b>Condition</b>: <code>${escape(
21+
weather.current.condition.text
22+
)}</code>\n` +
23+
`<b>Cloud Coverage</b>: <code>${weather.current.cloud}%</code>\n` +
24+
`<b>Wind</b>: <code>${weather.current.wind_kph}km/h</code>\n` +
25+
`<b>Last Updated</b>: <code>${Math.ceil(
26+
(localTime - weatherTime) / 60
27+
)} minutes ago</code>`
28+
);
29+
}
30+
31+
export function formatAstronomy(astronomy: Astronomy) {
32+
return (
33+
`<b>🌠 ${escape(
34+
`${astronomy.location.name}, ${astronomy.location.region}, ${astronomy.location.country}`
35+
)}</b>\n\n` +
36+
`<b>Time</b>: <code>${astronomy.location.localtime}</code>\n\n` +
37+
`<b>Astronomy</b>\n\n` +
38+
`<b>Sunrise</b>: <code>${astronomy.astronomy.astro.sunrise}</code>\n` +
39+
`<b>Sunset</b>: <code>${astronomy.astronomy.astro.sunset}</code>\n` +
40+
`<b>Moonrise</b>: <code>${astronomy.astronomy.astro.moonrise}</code>\n` +
41+
`<b>Moonset</b>: <code>${astronomy.astronomy.astro.moonset}</code>\n` +
42+
`<b>Moon Phase</b>: <code>${escape(
43+
astronomy.astronomy.astro.moon_phase
44+
)}</code>\n` +
45+
`<b>Moon Illumination</b>: <code>${astronomy.astronomy.astro.moon_illumination}%</code>\n` +
46+
`<b>Is Moon Up</b>: <code>${
47+
astronomy.astronomy.astro.is_moon_up ? "Yes" : "No"
48+
}</code>\n`
49+
);
50+
}
51+
52+
export function formatForecast(forecast: Forecast) {
53+
return (
54+
`<b>🌠 ${escape(
55+
`${forecast.location.name}, ${forecast.location.region}, ${forecast.location.country}`
56+
)}</b>\n\n` +
57+
`<b>Time</b>: <code>${forecast.location.localtime}</code>\n\n` +
58+
`<b>Forecast</b>\n\n` +
59+
`${forecast.forecast.forecastday
60+
.map(
61+
(ele) =>
62+
`<b>${ele.date}</b>\n` +
63+
`<b>Max 🌡</b>: <code>${ele.day.maxtemp_c}°C</code>\n` +
64+
`<b>Min 🌡</b>: <code>${ele.day.mintemp_c}°C</code>\n` +
65+
`<b>Chance of rain</b>: <code>${ele.day.daily_chance_of_rain}%</code>\n` +
66+
`<b>Chance of snow</b>: <code>${ele.day.daily_chance_of_snow}%</code>\n` +
67+
`<b>UV</b>: <code>${ele.day.uv}%</code>\n`
68+
)
69+
.join("\n")}`
70+
);
71+
}

weather/handler.ts

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Context } from "https://deno.land/x/[email protected]/context.ts";
2+
import { Filter } from "https://deno.land/x/[email protected]/filter.ts";
3+
import { InlineKeyboard } from "https://deno.land/x/[email protected]/mod.ts";
4+
5+
import {
6+
formatAstronomy,
7+
formatCurrentWeather,
8+
formatForecast,
9+
} from "./formatter.ts";
10+
import { Astronomy, Forecast, SearchLocation, Weather } from "./type.ts";
11+
12+
const API_URL = "https://api.weatherapi.com/v1";
13+
14+
const API_KEY = Deno.env.get("API_KEY");
15+
if (!API_KEY) throw new Error("API KEY is not set in the env!");
16+
17+
type APICallsType = "weather" | "astronomy" | "forecast" | "search";
18+
export async function api<Type extends APICallsType>(
19+
type: Type,
20+
query: string,
21+
extra = ""
22+
) {
23+
const url: Record<APICallsType, string> = {
24+
weather: `${API_URL}/current.json?key=${API_KEY}&q=${query}&${extra}`,
25+
astronomy: `${API_URL}/astronomy.json?key=${API_KEY}&q=${query}&${extra}`,
26+
forecast: `${API_URL}/forecast.json?key=${API_KEY}&q=${query}&days=3&aqi=no&alerts=no&${extra}`,
27+
search: `${API_URL}/search.json?key=${API_KEY}&q=${query}`,
28+
} as const;
29+
const response = await fetch(url[type]);
30+
if (response.ok) {
31+
const result = await response.json();
32+
return result as {
33+
forecast: Forecast;
34+
weather: Weather;
35+
astronomy: Astronomy;
36+
search: SearchLocation[];
37+
}[Type];
38+
}
39+
return null;
40+
}
41+
42+
export async function weather(
43+
ctx:
44+
| Filter<Context, "callback_query:data">
45+
| Filter<Context, "chosen_inline_result">,
46+
location: string,
47+
messageId: string,
48+
userId?: number
49+
) {
50+
const weather = await api("weather", location);
51+
if (!weather) return null;
52+
53+
return ctx.api.editMessageTextInline(
54+
messageId,
55+
formatCurrentWeather(weather),
56+
{
57+
parse_mode: "HTML",
58+
reply_markup: new InlineKeyboard()
59+
.text(
60+
"Astronomy",
61+
`${JSON.stringify({
62+
t: "astronomy",
63+
lc: location,
64+
uid: userId,
65+
})}`
66+
)
67+
.text(
68+
"Forecast",
69+
`${JSON.stringify({
70+
t: "forecast",
71+
lc: location,
72+
uid: userId,
73+
})}`
74+
),
75+
}
76+
);
77+
}
78+
79+
export async function astronomy(
80+
ctx: Filter<Context, "callback_query:data">,
81+
location: string,
82+
messageId: string,
83+
userId?: number
84+
) {
85+
const result = await api("astronomy", location);
86+
if (!result) return null;
87+
return ctx.api.editMessageTextInline(messageId, formatAstronomy(result), {
88+
parse_mode: "HTML",
89+
reply_markup: new InlineKeyboard()
90+
.text(
91+
"Forecast",
92+
`${JSON.stringify({ t: "forecast", lc: location, uid: userId })}`
93+
)
94+
.text(
95+
"Weather",
96+
`${JSON.stringify({ t: "weather", lc: location, uid: userId })}`
97+
),
98+
});
99+
}
100+
101+
export async function forecast(
102+
ctx: Filter<Context, "callback_query:data">,
103+
location: string,
104+
messageId: string,
105+
userId?: number
106+
) {
107+
const result = await api("forecast", location);
108+
if (!result) return null;
109+
return ctx.api.editMessageTextInline(messageId, formatForecast(result), {
110+
parse_mode: "HTML",
111+
reply_markup: new InlineKeyboard()
112+
.text(
113+
"Astronomy",
114+
`${JSON.stringify({ t: "astronomy", lc: location, uid: userId })}`
115+
)
116+
.text(
117+
"Weather",
118+
`${JSON.stringify({ t: "weather", lc: location, uid: userId })}`
119+
),
120+
});
121+
}

weather/mod.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { serve } from "https://deno.land/[email protected]/http/server.ts";
2+
import { webhookCallback } from "https://deno.land/x/[email protected]/mod.ts";
3+
4+
import { bot } from "./bot.ts";
5+
6+
const handleUpdate = webhookCallback(bot, "std/http");
7+
const SECRET = Deno.env.get("SECRET");
8+
9+
serve(async (req) => {
10+
if (req.method === "POST") {
11+
const url = new URL(req.url);
12+
if (url.pathname.slice(1) === SECRET) {
13+
try {
14+
return await handleUpdate(req);
15+
} catch (err) {
16+
console.error(err);
17+
}
18+
}
19+
}
20+
return new Response("Weather bot is up and running!");
21+
});

0 commit comments

Comments
 (0)