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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"start": "nodemon src/index.ts",
"dist": "tsc",
"test": "mocha --require ts-node/register --extension ts --spec './tests/**/*.ts'",
"test": "NODE_ENV=test mocha --require ts-node/register --extension ts --spec './tests/**/*.ts'",
"deploy": "docker compose build --no-cache && docker compose up -d --force-recreate"
},
"dependencies": {
Expand All @@ -18,6 +18,7 @@
"lodash": "^4.17.21",
"moment": "^2.30.1",
"pg": "^8.16.3",
"pino": "^9.5.0",
"telegraf": "^4.16.3"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { config } from './config/config'
import { startServer } from './server'
import PostgreSQL from './services/postgresql'
import logger from './logger'

export let database: PostgreSQL

Expand All @@ -10,7 +11,8 @@ const app = async () => {
database = new PostgreSQL(config.database)
await database.start()
} catch (error) {
console.error(error)
logger.error({ err: error }, 'Application bootstrap failed')
process.exitCode = 1
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pino from 'pino'

const level =
process.env.LOG_LEVEL ??
(process.env.NODE_ENV === 'test' ? 'silent' : process.env.NODE_ENV === 'production' ? 'info' : 'debug')

const logger = pino({
level,
base: {
service: 'mida-sync',
},
formatters: {
level: (label) => ({ level: label }),
},
})

export default logger

37 changes: 37 additions & 0 deletions src/models/alert-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { database } from '..'

const tableName = 'alert_reports'

export interface AlertReport {
id: number
report_number: string
created_on: string
starts_on: string
ends_on: string
emitted_on: string
estofex_sent: boolean
pretemp_sent: boolean
is_critic: boolean
}

type EditableAlertReport = Omit<AlertReport, 'id'>

export const getLastAlertReport = async (): Promise<AlertReport> => {
const query = `SELECT * FROM ${tableName} ORDER BY id DESC LIMIT 1`

const reports = await database.query<AlertReport>(query)

if (reports.length === 0) {
throw new Error('No last alert report found')
}

return reports[0]
}

export const createAlertReport = async (report: EditableAlertReport): Promise<AlertReport> => {
return database.create<AlertReport>(tableName, report)
}

export const updateLastAlertReport = async (report: Partial<EditableAlertReport>, id: number): Promise<AlertReport> => {
return database.edit<AlertReport>(tableName, report, id)
}
29 changes: 0 additions & 29 deletions src/models/last-alert-report.ts

This file was deleted.

26 changes: 0 additions & 26 deletions src/models/telegram-user.ts

This file was deleted.

20 changes: 13 additions & 7 deletions src/routes/forecast-reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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'
import { getLastAlertReport, updateLastAlertReport } from '../models/alert-report'

export const registerForecastReportsRoutes = (fastify) => {
fastify.route({
Expand All @@ -24,9 +24,12 @@ export const registerForecastReportsRoutes = (fastify) => {

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

await updateLastAlertReport({
pretemp_sent: true,
})
await updateLastAlertReport(
{
pretemp_sent: true,
},
lastAlertReport.id
)

reply.status(204).send(undefined)
},
Expand All @@ -53,9 +56,12 @@ export const registerForecastReportsRoutes = (fastify) => {

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

await updateLastAlertReport({
estofex_sent: true,
})
await updateLastAlertReport(
{
estofex_sent: true,
},
lastAlertReport.id
)

reply.status(204).send(undefined)
},
Expand Down
14 changes: 10 additions & 4 deletions src/routes/meteo-alerts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sendNewTomorrowAlertMessage } from '../utilites/telegram'
import { getLastAlertReport, updateLastAlertReport } from '../models/last-alert-report'
import { createAlertReport, getLastAlertReport } from '../models/alert-report'
import { getTomorrowMeteoAlert } from '../services/meteo-alerts'
import { parseMeteoAlert } from '../utilites/meteo-alerts'

Expand All @@ -18,14 +18,20 @@ export const registerMeteoAlertsRoutes = (fastify) => {

const lastAlertReport = await getLastAlertReport()

if (lastAlertReport.report_id !== parsedAlert.id) {
if (lastAlertReport.report_number !== parsedAlert.id) {
if (parsedAlert.isCritic) {
sendNewTomorrowAlertMessage(parsedAlert)
}

await updateLastAlertReport({
report_id: parsedAlert.id,
await createAlertReport({
report_number: parsedAlert.id,
is_critic: parsedAlert.isCritic,
estofex_sent: false,
pretemp_sent: false,
created_on: new Date().toISOString(),
starts_on: parsedAlert.dataInizio,
ends_on: parsedAlert.dataFine,
emitted_on: parsedAlert.dataEmissione,
})
}

Expand Down
7 changes: 5 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Fastify from 'fastify'
import Fastify, { type FastifyServerOptions } from 'fastify'
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'
import logger from './logger'

const translations = {
it: {
Expand All @@ -13,7 +14,7 @@ const translations = {

export const startServer = async () => {
const fastify = Fastify({
logger: true,
logger: logger as unknown as FastifyServerOptions['logger'],
disableRequestLogging: true,
})

Expand All @@ -36,4 +37,6 @@ export const startServer = async () => {
await fastify.listen({
port: 3000,
})

logger.info('HTTP server started on port 3000')
}
21 changes: 15 additions & 6 deletions src/services/estofex.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios'
import { XMLParser } from 'fast-xml-parser'
import logger from '../logger'

export interface EstofexReport {
forecast?: Partial<{
Expand All @@ -19,21 +20,29 @@ export interface EstofexReport {
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)
try {
const xmlData = await axios.get(xmlUrl).then((response) => response.data)

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

return parser.parse(xmlData) as EstofexReport
return parser.parse(xmlData) as EstofexReport
} catch (error) {
logger.error({ err: error }, 'Failed to fetch Estofex report')
throw error
}
}

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

try {
await axios.head(imageUrl)
} catch {}
} catch (error) {
logger.warn({ err: error }, 'Estofex image not available')
throw new Error('Estofex image not available')
}

return imageUrl
}
16 changes: 11 additions & 5 deletions src/services/meteo-alerts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios'
import customMoment from '../custom-components/custom-moment'
import logger from '../logger'

export enum MeteoAlertType {
green = 'green',
Expand Down Expand Up @@ -69,11 +70,16 @@ export const getMeteoAlert = async (date?: string): Promise<MeteoAlert | undefin
baseUrl += `?data=${date}`
}

const response = await axios.get<MeteoAlert | {}>(baseUrl).then((response) => response.data)
try {
const response = await axios.get<MeteoAlert | {}>(baseUrl).then((response) => response.data)

if (Object.keys(response).length === 0) {
return undefined
}
if (Object.keys(response).length === 0) {
return undefined
}

return response as MeteoAlert
return response as MeteoAlert
} catch (error) {
logger.error({ err: error, date }, 'Failed to retrieve meteo alert')
throw error
}
}
22 changes: 22 additions & 0 deletions src/services/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ export default class PostgreSQL {
}
}

public async create<T = any>(tableName: string, object: Omit<T, 'id'>) {
const keys = Object.keys(object)
.map((key) => checkAndTransformKey(key))
.join(', ')

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

const query = `INSERT INTO ${checkAndTransformKey(
tableName
)} (${keys}) VALUES (${values.map((_, i) => `$${i + 1}`).join(', ')}) RETURNING *`

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

return rows[0]
}

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}`)

Expand All @@ -104,6 +120,12 @@ export default class PostgreSQL {

return rows[0]
}

public async delete(tableName: string, itemId: number) {
const query = `DELETE FROM ${checkAndTransformKey(tableName)} WHERE id = $1`

await this.query<void>(query, [itemId])[0]
}
}

// Add backtick to sql reserved keywords
Expand Down
5 changes: 5 additions & 0 deletions src/services/pretemp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios from 'axios'
import moment from 'moment'
import { toFirstLetterUpperCase } from '../utilites/common'
import customMoment from '../custom-components/custom-moment'
import logger from '../logger'

export const getPretempReport = async (date: moment.Moment) => {
const formattedDate = date.format('DD_MM_YYYY')
Expand All @@ -25,6 +26,10 @@ export const getPretempReport = async (date: moment.Moment) => {
image = url
}

if (!image) {
logger.warn({ date: formattedDate }, 'Pretemp report unavailable')
}

return image
}

Expand Down
Loading