Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ RUN crontab -r || true
RUN crontab /etc/cron.d/mida-cron

# Create log directory for cron
RUN mkdir -p /var/log/cron && touch /var/log/cron/meteo_alerts.log
RUN mkdir -p /var/log/cron
RUN touch /var/log/cron/meteo_alerts.log
RUN touch /var/log/cron/check_pretemp_report.log
RUN touch /var/log/cron/check_estofex_report.log

EXPOSE 3000

Expand Down
2 changes: 2 additions & 0 deletions cron.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*/5 * * * * curl -X POST http://localhost:3000/meteo-alerts >> /var/log/cron/meteo_alerts.log 2>&1
*/5 * * * * curl -X POST http://localhost:3000/check-pretemp-report >> /var/log/cron/check_pretemp_report.log 2>&1
*/5 * * * * curl -X POST http://localhost:3000/check-estofex-report >> /var/log/cron/check_estofex_report.log 2>&1
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"axios": "^1.10.0",
"dotenv": "^17.2.0",
"fast-xml-parser": "^5.2.5",
"fastify": "^5.4.0",
"i18next": "^25.3.2",
"lodash": "^4.17.21",
Expand All @@ -20,17 +21,17 @@
"telegraf": "^4.16.3"
},
"devDependencies": {
"@types/chai": "^5.2.2",
"@types/chai": "^4.3.6",
"@types/lodash": "^4.17.20",
"@types/mocha": "^10.0.1",
"@types/node": "22",
"@types/pg": "^8.15.4",
"@types/sinon": "^17.0.4",
"chai": "^5.2.1",
"@types/sinon": "^17.0.3",
"chai": "^4.3.8",
"mocha": "^10.2.0",
"nodemon": "^3.1.10",
"prettier": "^3.6.2",
"sinon": "^21.0.0",
"sinon": "^18.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
Expand Down
11 changes: 7 additions & 4 deletions src/models/last-alert-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ const tableName = 'django_models_latestreport'
export interface LastAlertReport {
id: number
report_id: string
is_critic: boolean
estofex_sent: boolean
pretemp_sent: boolean
}

type EditableLastAlertReport = Omit<LastAlertReport, 'id'>

export const getLastAlertReport = async (): Promise<LastAlertReport> => {
const query = `SELECT * FROM ${tableName} `

Expand All @@ -19,8 +24,6 @@ export const getLastAlertReport = async (): Promise<LastAlertReport> => {
return reports[0]
}

export const updateLastAlertReport = async (reportId: string): Promise<void> => {
const query = `UPDATE ${tableName} SET report_id = $1 WHERE id = 1`

await database.query(query, [reportId])
export const updateLastAlertReport = async (report: Partial<EditableLastAlertReport>): Promise<void> => {
await database.edit<LastAlertReport>(tableName, report, 1)
}
63 changes: 63 additions & 0 deletions src/routes/forecast-reports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { config } from '../config/config'
import { checkEstofexReport } from '../utilites/estofex'
import { getEstofexImage, getEstofexReport } from '../services/estofex'
import { getTomorrowPretempReport } from '../services/pretemp'
import { sendPhotoMessage } from '../services/telegram'
import { getLastAlertReport, updateLastAlertReport } from '../models/last-alert-report'

export const registerForecastReportsRoutes = (fastify) => {
fastify.route({
method: 'POST',
url: '/check-pretemp-report',
handler: async (_, reply) => {
const lastAlertReport = await getLastAlertReport()

if (!lastAlertReport.is_critic || lastAlertReport.pretemp_sent) {
return reply.status(204).send(undefined)
}

const tomorrowReport = await getTomorrowPretempReport()

if (!tomorrowReport) {
return reply.status(204).send(undefined)
}

await sendPhotoMessage(config.chat_id, tomorrowReport, 'Nuovo report Pretemp disponibile')

await updateLastAlertReport({
pretemp_sent: true,
})

reply.status(204).send(undefined)
},
})
fastify.route({
method: 'POST',
url: '/check-estofex-report',
handler: async (_, reply) => {
const lastAlertReport = await getLastAlertReport()

if (!lastAlertReport.is_critic || lastAlertReport.estofex_sent) {
return reply.status(204).send(undefined)
}

const tomorrowReport = await getEstofexReport()

const isReportValid = checkEstofexReport(tomorrowReport)

if (!isReportValid) {
return reply.status(204).send(undefined)
}

const estofexImage = await getEstofexImage()

await sendPhotoMessage(config.chat_id, estofexImage, 'Nuovo report Estofex disponibile')

await updateLastAlertReport({
estofex_sent: true,
})

reply.status(204).send(undefined)
},
})
}
7 changes: 5 additions & 2 deletions src/routes/meteo-alerts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sendNewTomorrowAlertMessage } from '../controllers/telegram'
import { sendNewTomorrowAlertMessage } from '../utilites/telegram'
import { getLastAlertReport, updateLastAlertReport } from '../models/last-alert-report'
import { getTomorrowMeteoAlert } from '../services/meteo-alerts'
import { parseMeteoAlert } from '../utilites/meteo-alerts'
Expand All @@ -23,7 +23,10 @@ export const registerMeteoAlertsRoutes = (fastify) => {
sendNewTomorrowAlertMessage(parsedAlert)
}

await updateLastAlertReport(parsedAlert.id)
await updateLastAlertReport({
report_id: parsedAlert.id,
is_critic: parsedAlert.isCritic,
})
}

reply.status(200).send(parsedAlert)
Expand Down
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { registerMeteoAlertsRoutes } from './routes/meteo-alerts'
import i18next from 'i18next'
import italian from './resources/locales/it.json'
import { registerTestMessageRoutes } from './routes/test-message'
import { registerForecastReportsRoutes } from './routes/forecast-reports'

const translations = {
it: {
Expand Down Expand Up @@ -30,6 +31,7 @@ export const startServer = async () => {

registerTestMessageRoutes(fastify)
registerMeteoAlertsRoutes(fastify)
registerForecastReportsRoutes(fastify)

await fastify.listen({
port: 3000,
Expand Down
39 changes: 39 additions & 0 deletions src/services/estofex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import axios from 'axios'
import { XMLParser } from 'fast-xml-parser'

export interface EstofexReport {
forecast?: Partial<{
forecast_type: string
start_time: Partial<{
'@_value': string
}>
expiry_time: Partial<{
'@_value': string
}>
issue_time: Partial<{
'@_value': string
}>
}>
}

export const getEstofexReport = async () => {
const xmlUrl = 'https://www.estofex.org/cgi-bin/polygon/showforecast.cgi?xml=yes'

const xmlData = await axios.get(xmlUrl).then((response) => response.data)

const parser = new XMLParser({
ignoreAttributes: false,
})

return parser.parse(xmlData) as EstofexReport
}

export const getEstofexImage = async () => {
const imageUrl = 'https://www.estofex.org/forecasts/tempmap/.png'

try {
await axios.head(imageUrl)
} catch {}

return imageUrl
}
19 changes: 19 additions & 0 deletions src/services/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,23 @@ export default class PostgreSQL {
throw new Error(error)
}
}

public async edit<T>(tableName: string, object: Omit<Partial<T>, 'id'>, objectId: number) {
const keys = Object.keys(object).map((key, index) => `${checkAndTransformKey(key)} = $${index + 1}`)

const values: any[] = Object.values(object)

const query = `UPDATE ${checkAndTransformKey(tableName)} ${tableName} SET ${keys} WHERE id = $${keys.length + 1} RETURNING *`

const rows = await this.query<T>(query, [...values, objectId])

return rows[0]
}
}

// Add backtick to sql reserved keywords
export const checkAndTransformKey = (key: string): string => {
const protectedKeywords = ['key', 'table', 'group', 'from', 'desc', 'condition', 'before', 'grant', 'user', 'is']

return protectedKeywords.includes(key) ? `"${key}"` : key
}
34 changes: 34 additions & 0 deletions src/services/pretemp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import axios from 'axios'
import moment from 'moment'
import { toFirstLetterUpperCase } from '../utilites/common'
import customMoment from '../custom-components/custom-moment'

export const getPretempReport = async (date: moment.Moment) => {
const formattedDate = date.format('DD_MM_YYYY')
const month = date.format('MMMM').toLowerCase()
const urls = [
`https://pretemp.altervista.org/archivio/${date.year()}/${month}/cartine/${formattedDate}.png`,
`https://pretemp.altervista.org/archivio/${date.year()}/${toFirstLetterUpperCase(month)}/cartine/${formattedDate}.png`,
]

let image: string | undefined = undefined

for (let i = 0; i < urls.length; i++) {
const url = urls[i]

try {
await axios.head(url)
} catch {
continue
}

image = url
}

return image
}

export const getTomorrowPretempReport = async () => {
const tomorrow = customMoment().add(1, 'day')
return getPretempReport(tomorrow)
}
4 changes: 4 additions & 0 deletions src/services/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ const bot = new Telegram.Telegraf(config.telegram_token)
export const sendTelegramMessage = async (chatId: string, text: string, extra?: Telegram.Types.ExtraReplyMessage) => {
await bot.telegram.sendMessage(chatId, text, extra)
}

export const sendPhotoMessage = async (chatId: string, photoUrl: string, caption?: string, extra?: Telegram.Types.ExtraPhoto) => {
await bot.telegram.sendPhoto(chatId, photoUrl, { caption, ...extra })
}
4 changes: 4 additions & 0 deletions src/utilites/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import i18next, { TOptions } from 'i18next'
export const translateKey = (key: string, language: string, options: TOptions = {}) => {
return i18next.t(key, { lng: language, ...options })
}

export const toFirstLetterUpperCase = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
19 changes: 19 additions & 0 deletions src/utilites/estofex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import customMoment from '../custom-components/custom-moment'
import { EstofexReport } from '../services/estofex'

export const checkEstofexReport = (report: EstofexReport): boolean => {
if (!report.forecast || !report.forecast.start_time || !report.forecast.expiry_time) {
return false
}

if (!report.forecast.start_time['@_value'] || !report.forecast.expiry_time['@_value']) {
return false
}

const startTime = customMoment(parseInt(report.forecast.start_time['@_value'], 10))
const expiryTime = customMoment(parseInt(report.forecast.expiry_time['@_value'], 10))

const tomorrow = customMoment().add(1, 'day')

return startTime.isBefore(tomorrow) && expiryTime.isAfter(tomorrow)
}
4 changes: 2 additions & 2 deletions src/controllers/telegram.ts → src/utilites/telegram.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sendTelegramMessage } from '../services/telegram'
import { InlineKeyboardButton } from 'telegraf/typings/core/types/typegram'
import { translateKey } from '../utilites/common'
import { ParsedMeteoAlert } from '../utilites/meteo-alerts'
import { translateKey } from './common'
import { ParsedMeteoAlert } from './meteo-alerts'
import { config } from '../config/config'

const separator = '--------------------------------'
Expand Down
34 changes: 34 additions & 0 deletions tests/utilities/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from 'chai'
import { toFirstLetterUpperCase } from '../../src/utilites/common'

describe('toFirstLetterUpperCase', () => {
it('capitalizes the first letter of a lowercase word', () => {
expect(toFirstLetterUpperCase('hello')).to.equal('Hello')
})

it('returns the same string if first letter is already uppercase', () => {
expect(toFirstLetterUpperCase('Hello')).to.equal('Hello')
})

it('returns empty string unchanged', () => {
expect(toFirstLetterUpperCase('')).to.equal('')
})

it('capitalizes a single-letter string', () => {
expect(toFirstLetterUpperCase('a')).to.equal('A')
})

it('does not change strings that start with a non-letter character', () => {
expect(toFirstLetterUpperCase('1abc')).to.equal('1abc')
expect(toFirstLetterUpperCase('!bang')).to.equal('!bang')
})

it('capitalizes unicode first letters', () => {
expect(toFirstLetterUpperCase('éclair')).to.equal('Éclair')
})

it('only changes the first character and leaves the rest intact', () => {
expect(toFirstLetterUpperCase('hELLO')).to.equal('HELLO')
expect(toFirstLetterUpperCase('hello world')).to.equal('Hello world')
})
})
Loading