Compare commits
38 commits
bring_back
...
master
Author | SHA1 | Date | |
---|---|---|---|
3d8306b225 | |||
d3460f8871 | |||
15932a0c9d | |||
3fef736821 | |||
486f334b4a | |||
e133b49ee9 | |||
59552698e9 | |||
3ad063cbc2 | |||
55b7b99e48 | |||
ea7e224c9e | |||
d5408c359b | |||
265a182d8f | |||
dbdf76554b | |||
daea0c9cfb | |||
54483b96b6 | |||
522d30d7dd | |||
dc57073067 | |||
0362566c2a | |||
b07d434bf3 | |||
5588c999a7 | |||
fd7e2377e1 | |||
427423d3e8 | |||
87582b72ee | |||
1f59ccfb2c | |||
3aeb79f054 | |||
92a256c784 | |||
fd4b483a6c | |||
f95ed86012 | |||
95b2f4283b | |||
43bb990df5 | |||
a1ae8c98d1 | |||
bbf1865f1a | |||
7675d627d8 | |||
18c3fcb06a | |||
a022002c36 | |||
d408a385bb | |||
|
90bb926341 | ||
|
0fea123354 |
17 changed files with 845 additions and 874 deletions
|
@ -1,9 +1,11 @@
|
||||||
|
# Google
|
||||||
|
SERPAPI_KEY
|
||||||
|
|
||||||
# Google
|
# Google
|
||||||
GOOGLE_CALENDAR_ID
|
GOOGLE_CALENDAR_ID
|
||||||
GOOGLE_PROJECT_NUMBER
|
GOOGLE_PROJECT_NUMBER
|
||||||
GOOGLE_CLIENT_EMAIL
|
GOOGLE_CLIENT_EMAIL
|
||||||
GOOGLE_PRIVATE_KEY
|
GOOGLE_PRIVATE_KEY
|
||||||
GOOGLE_KEY_ID
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID
|
GOOGLE_CLIENT_ID
|
||||||
GOOGLE_CLIENT_SECRET
|
GOOGLE_CLIENT_SECRET
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -2,8 +2,8 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
#javascript build files #
|
#javascript build files #
|
||||||
public/**/*
|
# public/**/*
|
||||||
dist/**/*
|
dist/**/*
|
||||||
|
|
||||||
# Tmp files #
|
# Tmp files #
|
||||||
tmp
|
tmp
|
||||||
|
@ -19,3 +19,5 @@ push_dockerhub.sh
|
||||||
|
|
||||||
# keys #
|
# keys #
|
||||||
keys/**/*
|
keys/**/*
|
||||||
|
|
||||||
|
cron.log
|
18
Dockerfile
18
Dockerfile
|
@ -1,28 +1,16 @@
|
||||||
FROM node:16
|
FROM node:16
|
||||||
|
|
||||||
# Create app directory
|
ENV TZ=Asia/Jerusalem
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Install app dependencies
|
|
||||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
|
||||||
# where available (npm@5+)
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
RUN npm install -g typescript
|
RUN npm install -g typescript
|
||||||
|
|
||||||
# If you are building your code for production
|
|
||||||
# RUN npm ci --omit=dev
|
|
||||||
|
|
||||||
# Bundle app source
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
# run npm build to create the dist folder
|
|
||||||
|
|
||||||
RUN tsc
|
RUN tsc
|
||||||
# run npm start to start the server
|
|
||||||
CMD [ "npm", "start" ]
|
CMD [ "npm", "start" ]
|
||||||
|
|
||||||
|
|
95
dist/GameSource.js
vendored
Normal file
95
dist/GameSource.js
vendored
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
require("dotenv").config();
|
||||||
|
const axios_1 = __importDefault(require("axios"));
|
||||||
|
const moment_1 = __importDefault(require("moment"));
|
||||||
|
class GameSource {
|
||||||
|
async getGamesFromHaifa(logger) {
|
||||||
|
console.log("Trying to get games from Haifa...");
|
||||||
|
try {
|
||||||
|
// Get the current date and time in the required format
|
||||||
|
const currentDate = (0, moment_1.default)().format("DD/MM/YYYY HH:mm");
|
||||||
|
// Construct the filters object with the current date
|
||||||
|
const filters = {
|
||||||
|
date: {
|
||||||
|
startDate: currentDate,
|
||||||
|
endDate: "",
|
||||||
|
},
|
||||||
|
league: "",
|
||||||
|
session: "",
|
||||||
|
gamesDirection: "1",
|
||||||
|
};
|
||||||
|
// Encode the filters for the URL
|
||||||
|
const filtersParam = encodeURIComponent(JSON.stringify(filters));
|
||||||
|
// Construct the API URL with the encoded filters
|
||||||
|
const sourceUrl = `https://api.mhaifafc.com/api/content/games-lobby?filters=${filtersParam}&start=0&limit=20&sortDirection=ASC&app=web&lang=he`;
|
||||||
|
// Get the authorization token from environment variables
|
||||||
|
const authorizationToken = process.env.HAIFA_API_AUTH_TOKEN;
|
||||||
|
// Set up the request headers
|
||||||
|
const headers = {
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.7",
|
||||||
|
Authorization: `Bearer ${authorizationToken}`,
|
||||||
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
Origin: "https://www.mhaifafc.com",
|
||||||
|
Referer: "https://www.mhaifafc.com/",
|
||||||
|
};
|
||||||
|
// Make the API request
|
||||||
|
const response = await axios_1.default.get(sourceUrl, {
|
||||||
|
headers,
|
||||||
|
responseType: "json",
|
||||||
|
responseEncoding: "utf8", // Ensure UTF-8 encoding
|
||||||
|
});
|
||||||
|
// Extract the games data from the response
|
||||||
|
const gamesData = response.data.games.items;
|
||||||
|
const games = [];
|
||||||
|
// Loop through each game and construct the GoogleCalendarEvent objects
|
||||||
|
for (const game of gamesData) {
|
||||||
|
const gameDetails = game.gameDetails;
|
||||||
|
const gameTime = gameDetails.gameTime; // ISO string
|
||||||
|
const isFinalGameDate = gameDetails.isFinalGameDate;
|
||||||
|
const gameLocation = gameDetails.gameLocation;
|
||||||
|
// Skip games without a game time
|
||||||
|
if (!gameTime)
|
||||||
|
continue;
|
||||||
|
const hostTeam = game.hostTeam;
|
||||||
|
const guestTeam = game.guestTeam;
|
||||||
|
// Get team names
|
||||||
|
const hostTeamName = hostTeam.teamName;
|
||||||
|
const guestTeamName = guestTeam.teamName;
|
||||||
|
const summary = `${hostTeamName} vs. ${guestTeamName}`;
|
||||||
|
// Include a note if the game date is not final
|
||||||
|
let description = `${hostTeamName} vs. ${guestTeamName}`;
|
||||||
|
if (!isFinalGameDate) {
|
||||||
|
description += " (Date and time are subject to change)";
|
||||||
|
}
|
||||||
|
// Calculate start and end times
|
||||||
|
const startDateTime = (0, moment_1.default)(gameTime).toISOString();
|
||||||
|
const endDateTime = (0, moment_1.default)(gameTime).add(2, "hours").toISOString();
|
||||||
|
// Add the event to the games array
|
||||||
|
games.push({
|
||||||
|
summary: summary,
|
||||||
|
location: gameLocation,
|
||||||
|
description: description,
|
||||||
|
start: {
|
||||||
|
dateTime: startDateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: endDateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.default = GameSource;
|
88
dist/GoogleCalendar.js
vendored
Normal file
88
dist/GoogleCalendar.js
vendored
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const google_auth_library_1 = require("google-auth-library");
|
||||||
|
const googleapis_1 = require("googleapis");
|
||||||
|
require("dotenv").config();
|
||||||
|
const env = process.env;
|
||||||
|
class GoogleCalendar {
|
||||||
|
constructor() {
|
||||||
|
this.gamesMap = {};
|
||||||
|
this.clientSecret = env.GOOGLE_CLIENT_SECRET;
|
||||||
|
this.clientId = env.GOOGLE_CLIENT_ID;
|
||||||
|
this.calenderId = env.GOOGLE_CALENDAR_ID;
|
||||||
|
this.clientEmail = env.GOOGLE_CLIENT_EMAIL;
|
||||||
|
this.googlePrivateKey = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
console.log("INIT GOOGLE CALENDAR");
|
||||||
|
const jwtClient = await this.authorize();
|
||||||
|
this.calendar = googleapis_1.google.calendar({ version: "v3", auth: jwtClient });
|
||||||
|
}
|
||||||
|
async authorize() {
|
||||||
|
console.log("AUTHORIZE GOOGLE CALENDAR");
|
||||||
|
this.JWT_client = new google_auth_library_1.JWT({
|
||||||
|
email: this.clientEmail,
|
||||||
|
key: this.googlePrivateKey,
|
||||||
|
scopes: ["https://www.googleapis.com/auth/calendar"],
|
||||||
|
});
|
||||||
|
const { access_token } = await this.JWT_client.authorize();
|
||||||
|
this.token = access_token;
|
||||||
|
if (!this.token) {
|
||||||
|
throw new Error("Failed to connect to google calendar");
|
||||||
|
}
|
||||||
|
console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY");
|
||||||
|
return this.JWT_client;
|
||||||
|
}
|
||||||
|
async updateNewEvent(upcomingEvents) {
|
||||||
|
// console.log(upcomingEvents)
|
||||||
|
setTimeout(async () => {
|
||||||
|
upcomingEvents.forEach(async (event) => {
|
||||||
|
console.log("UPDATE NEW EVENT", upcomingEvents);
|
||||||
|
const options = {
|
||||||
|
auth: this.JWT_client,
|
||||||
|
calendarId: this.calenderId,
|
||||||
|
resource: {
|
||||||
|
summary: event.summary,
|
||||||
|
location: event.location,
|
||||||
|
description: event.description,
|
||||||
|
start: {
|
||||||
|
dateTime: event.start.dateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
end: { dateTime: event.end.dateTime, timeZone: "Asia/Jerusalem" },
|
||||||
|
sendNotifications: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await this.calendar.events.insert(options, function (err, event) {
|
||||||
|
if (err) {
|
||||||
|
console.log("There was an error contacting the Calendar service: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(event.description + " created");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
async isDuplicateEvent(startTime, endTime, title) {
|
||||||
|
if (this.gamesMap[startTime]) {
|
||||||
|
console.log("duplicate event");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.gamesMap[startTime] = true;
|
||||||
|
console.log("checking for duplicate event");
|
||||||
|
try {
|
||||||
|
const response = await this.calendar.events.list({
|
||||||
|
calendarId: this.calenderId,
|
||||||
|
timeMin: startTime,
|
||||||
|
timeMax: endTime,
|
||||||
|
q: title, // Search for events with the same title
|
||||||
|
});
|
||||||
|
return response.data.items.length > 0;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.default = GoogleCalendar;
|
68
dist/index.js
vendored
Normal file
68
dist/index.js
vendored
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const GameSource_1 = __importDefault(require("./GameSource"));
|
||||||
|
const GoogleCalendar_1 = __importDefault(require("./GoogleCalendar"));
|
||||||
|
const dotenv_1 = __importDefault(require("dotenv"));
|
||||||
|
const node_cron_1 = __importDefault(require("node-cron"));
|
||||||
|
const fs_1 = __importDefault(require("fs")); // Importing fs for logging
|
||||||
|
dotenv_1.default.config();
|
||||||
|
class App {
|
||||||
|
constructor() {
|
||||||
|
this.gameSource = new GameSource_1.default();
|
||||||
|
this.googleCalendar = new GoogleCalendar_1.default();
|
||||||
|
}
|
||||||
|
async startCronJob() {
|
||||||
|
this.writeLog('START Haifa Reminder'); // Log when the cron job starts
|
||||||
|
const newGamesAdded = [];
|
||||||
|
await this.googleCalendar.init();
|
||||||
|
try {
|
||||||
|
const games = await this.gameSource.getGamesFromHaifa(this.writeLog);
|
||||||
|
for (const game of games) {
|
||||||
|
const isDuplicateEvent = await this.googleCalendar.isDuplicateEvent(game.start.dateTime, game.end.dateTime, game.summary);
|
||||||
|
console.log(game);
|
||||||
|
if (!isDuplicateEvent) {
|
||||||
|
newGamesAdded.push(game);
|
||||||
|
console.log("Event does not exist");
|
||||||
|
await this.googleCalendar.updateNewEvent([game]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Event already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newGamesAdded.length > 0) {
|
||||||
|
console.log("New games added:", newGamesAdded);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("No new games were Added!");
|
||||||
|
}
|
||||||
|
this.writeLog('Successfully ran project');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this.writeLog("Error in cron job:" + error.message);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.writeLog('END CRON JOB'); // Log when the cron job ends
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeLog(message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logMessage = `${timestamp} - ${message}\n`;
|
||||||
|
// Write to log file synchronously
|
||||||
|
fs_1.default.appendFileSync('cron.log', logMessage);
|
||||||
|
console.log(logMessage); // Optional: also log to console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App();
|
||||||
|
node_cron_1.default.schedule('* * * * *', () => {
|
||||||
|
console.log('Running startCronJob at 10:00 AM Jerusalem time');
|
||||||
|
app.startCronJob().catch((error) => {
|
||||||
|
console.error("Error in scheduled cron job:", error.message);
|
||||||
|
app.writeLog(`ERROR: ${error.message}`); // Log any errors
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: "Asia/Jerusalem"
|
||||||
|
});
|
2
dist/types/index.js
vendored
Normal file
2
dist/types/index.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
939
package-lock.json
generated
939
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"cron": "^2.3.0",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"googleapis": "^122.0.0",
|
||||||
"ical": "^0.8.0",
|
|
||||||
"ics": "^3.1.0",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"moment-timezone": "^0.5.43",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"node-html-parser": "^6.1.5",
|
"node-html-parser": "^6.1.5",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
@ -18,6 +17,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/node": "^18.15.5",
|
"@types/node": "^18.15.5",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker stop haifareminder
|
docker stop haifareminder
|
||||||
docker rm haifareminder
|
docker rm haifareminder
|
||||||
docker run -p 3000:3000 -d --name haifareminder docker.io/kfda89/haifareminder
|
docker run -p 3000:3000 -d --restart=unless-stopped --name haifareminder docker.io/kfda89/haifareminder
|
|
@ -1,63 +1,106 @@
|
||||||
require('dotenv').config();
|
require("dotenv").config();
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { GoogleCalendarEvent } from "./types";
|
import { GoogleCalendarEvent } from "./types";
|
||||||
import { parse } from 'node-html-parser';
|
import moment from "moment";
|
||||||
import moment from 'moment'; // require
|
|
||||||
|
|
||||||
// This calss will be the game source.
|
|
||||||
// search for upcomming games
|
|
||||||
|
|
||||||
export default class GameSource {
|
export default class GameSource {
|
||||||
|
async getGamesFromHaifa(logger: Function): Promise<GoogleCalendarEvent[]> {
|
||||||
|
console.log("Trying to get games from Haifa...");
|
||||||
|
|
||||||
async getGamesFromHaifa(): Promise<GoogleCalendarEvent[]> {
|
try {
|
||||||
const sourceUrl = `https://mhaifafc.com/games?lang=en`;
|
// Get the current date and time in the required format
|
||||||
const result = await axios.get(sourceUrl);
|
const currentDate = moment().format("DD/MM/YYYY HH:mm");
|
||||||
const parsedResult = parse(result.data.toString())
|
|
||||||
const gameBoxs = parsedResult.querySelectorAll(".game-box");
|
|
||||||
|
|
||||||
const games: GoogleCalendarEvent[] = [];
|
// Construct the filters object with the current date
|
||||||
|
const filters = {
|
||||||
for (let gameBox of gameBoxs) {
|
date: {
|
||||||
|
startDate: currentDate,
|
||||||
const teamsPlaying = gameBox.querySelectorAll(".team-name").map((team: any) => team.text);
|
endDate: "",
|
||||||
const regex = /[\r\n\s]+/g;
|
|
||||||
const gameHeader = gameBox.querySelector(".game-header").text.replace(regex, " ").trim();
|
|
||||||
const headerSplit = gameHeader.split(",");
|
|
||||||
// In data, if there is no time, it means it's the last game for the calender
|
|
||||||
const lastGameForCalender = headerSplit.length < 4;
|
|
||||||
if (lastGameForCalender) break;
|
|
||||||
|
|
||||||
const gameDate = headerSplit[2].trim();
|
|
||||||
const gameTime = headerSplit[3].trim();
|
|
||||||
|
|
||||||
const startOriginal = moment(gameDate + gameTime, "DD/MM/YYYYHH:mm").format("DD/MM/YYYY HH:mm");
|
|
||||||
const endOriginal = moment(gameDate + gameTime, "DD/MM/YYYYHH:mm").add(2, "hours").format("DD/MM/YYYY HH:mm");
|
|
||||||
|
|
||||||
const start = [moment(startOriginal, 'DD/MM/YYYYHH:mm').year(), moment(startOriginal, 'DD/MM/YYYYHH:mm').month() + 1, moment(startOriginal, 'DD/MM/YYYYHH:mm').date(), moment(startOriginal, 'DD/MM/YYYYHH:mm').hour(), moment(startOriginal, 'DD/MM/YYYYHH:mm').minute()];
|
|
||||||
const end = [moment(endOriginal, 'DD/MM/YYYYHH:mm').year(), moment(endOriginal, 'DD/MM/YYYYHH:mm').month() + 1, moment(endOriginal, 'DD/MM/YYYYHH:mm').date(), moment(endOriginal, 'DD/MM/YYYYHH:mm').hour(), moment(endOriginal, 'DD/MM/YYYYHH:mm').minute()]
|
|
||||||
|
|
||||||
games.push({
|
|
||||||
summary: 'Maccabi Haifa F.C.',
|
|
||||||
location: "Sammy Ofer Stadium",
|
|
||||||
description: `${teamsPlaying[0]} vs. ${teamsPlaying[1]}`,
|
|
||||||
start: {
|
|
||||||
dateTime: start,
|
|
||||||
timeZone: 'Asia/Jerusalem'
|
|
||||||
},
|
},
|
||||||
end: {
|
league: "",
|
||||||
dateTime: end,
|
session: "",
|
||||||
timeZone: 'Asia/Jerusalem'
|
gamesDirection: "1",
|
||||||
}
|
};
|
||||||
})
|
|
||||||
}
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOpponentIndexByStadium(stadium: string) {
|
// Encode the filters for the URL
|
||||||
if (stadium === "Sammy Ofer Stadium") {
|
const filtersParam = encodeURIComponent(JSON.stringify(filters));
|
||||||
return 1;
|
|
||||||
} else {
|
// Construct the API URL with the encoded filters
|
||||||
return 0;
|
const sourceUrl = `https://api.mhaifafc.com/api/content/games-lobby?filters=${filtersParam}&start=0&limit=20&sortDirection=ASC&app=web&lang=he`;
|
||||||
|
|
||||||
|
// Get the authorization token from environment variables
|
||||||
|
const authorizationToken = process.env.HAIFA_API_AUTH_TOKEN;
|
||||||
|
|
||||||
|
// Set up the request headers
|
||||||
|
const headers = {
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.7",
|
||||||
|
Authorization: `Bearer ${authorizationToken}`,
|
||||||
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
Origin: "https://www.mhaifafc.com",
|
||||||
|
Referer: "https://www.mhaifafc.com/",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the API request
|
||||||
|
const response = await axios.get(sourceUrl, {
|
||||||
|
headers,
|
||||||
|
responseType: "json", // Ensure the response is parsed as JSON
|
||||||
|
responseEncoding: "utf8", // Ensure UTF-8 encoding
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract the games data from the response
|
||||||
|
const gamesData = response.data.games.items;
|
||||||
|
|
||||||
|
const games: GoogleCalendarEvent[] = [];
|
||||||
|
|
||||||
|
// Loop through each game and construct the GoogleCalendarEvent objects
|
||||||
|
for (const game of gamesData) {
|
||||||
|
const gameDetails = game.gameDetails;
|
||||||
|
const gameTime = gameDetails.gameTime; // ISO string
|
||||||
|
const isFinalGameDate = gameDetails.isFinalGameDate;
|
||||||
|
const gameLocation = gameDetails.gameLocation;
|
||||||
|
|
||||||
|
// Skip games without a game time
|
||||||
|
if (!gameTime) continue;
|
||||||
|
|
||||||
|
const hostTeam = game.hostTeam;
|
||||||
|
const guestTeam = game.guestTeam;
|
||||||
|
|
||||||
|
// Get team names
|
||||||
|
const hostTeamName = hostTeam.teamName;
|
||||||
|
const guestTeamName = guestTeam.teamName;
|
||||||
|
|
||||||
|
const summary = `${hostTeamName} vs. ${guestTeamName}`;
|
||||||
|
|
||||||
|
// Include a note if the game date is not final
|
||||||
|
let description = `${hostTeamName} vs. ${guestTeamName}`;
|
||||||
|
if (!isFinalGameDate) {
|
||||||
|
description += " (Date and time are subject to change)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate start and end times
|
||||||
|
const startDateTime = moment(gameTime).toISOString();
|
||||||
|
const endDateTime = moment(gameTime).add(2, "hours").toISOString();
|
||||||
|
|
||||||
|
// Add the event to the games array
|
||||||
|
games.push({
|
||||||
|
summary: summary,
|
||||||
|
location: gameLocation,
|
||||||
|
description: description,
|
||||||
|
start: {
|
||||||
|
dateTime: startDateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: endDateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return games;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
// import { JWT } from 'google-auth-library';
|
|
||||||
// import { google } from 'googleapis';
|
|
||||||
// import { GoogleCalendarEvent } from './types/index';
|
|
||||||
|
|
||||||
// require('dotenv').config();
|
|
||||||
// const env = process.env;
|
|
||||||
|
|
||||||
|
|
||||||
// export default class GoogleCalendar {
|
|
||||||
// clientSecret: string = env.GOOGLE_CLIENT_SECRET;
|
|
||||||
// clientId: string = env.GOOGLE_CLIENT_ID;
|
|
||||||
// calenderId: string = env.GOOGLE_CALENDAR_ID;
|
|
||||||
// calendar: any;
|
|
||||||
// clientEmail: string = env.GOOGLE_CLIENT_EMAIL;
|
|
||||||
// googlePrivateKey: string = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n');
|
|
||||||
// token: any;
|
|
||||||
// JWT_client: JWT;
|
|
||||||
|
|
||||||
// async init() {
|
|
||||||
// console.log("INIT GOOGLE CALENDAR")
|
|
||||||
// const jwtClient = await this.authorize();
|
|
||||||
// this.calendar = google.calendar({ version: 'v3', auth: jwtClient });
|
|
||||||
// console.log("DONE INIT GOOGLE CALENDAR")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async authorize() {
|
|
||||||
// console.log("AUTHORIZE GOOGLE CALENDAR")
|
|
||||||
// this.JWT_client = new JWT({
|
|
||||||
// email: this.clientEmail,
|
|
||||||
// key: this.googlePrivateKey,
|
|
||||||
// scopes: [
|
|
||||||
// 'https://www.googleapis.com/auth/calendar',
|
|
||||||
// ]
|
|
||||||
// });
|
|
||||||
// const { access_token } = await this.JWT_client.authorize();
|
|
||||||
// this.token = access_token;
|
|
||||||
// if (!this.token) {
|
|
||||||
// throw new Error('Failed to connect to google calendar');
|
|
||||||
// }
|
|
||||||
// console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY")
|
|
||||||
// return this.JWT_client;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async updateNewEvent(upcomingEvents: GoogleCalendarEvent[]) {
|
|
||||||
// setTimeout(async () => {
|
|
||||||
// upcomingEvents.forEach((event: GoogleCalendarEvent) => {
|
|
||||||
// console.log("UPDATE NEW EVENT", upcomingEvents)
|
|
||||||
// const options = {
|
|
||||||
// auth: this.JWT_client,
|
|
||||||
// calendarId: this.calenderId,
|
|
||||||
// resource: event,
|
|
||||||
// }
|
|
||||||
// this.calendar.events.insert(options, function (err: any, event: any) {
|
|
||||||
// if (err) {
|
|
||||||
// console.log('There was an error contacting the Calendar service: ' + err);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// console.log(event.description + ' created');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// })
|
|
||||||
// }, 3000)
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
97
src/GoogleCalendar.ts
Normal file
97
src/GoogleCalendar.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { JWT } from "google-auth-library";
|
||||||
|
import { google } from "googleapis";
|
||||||
|
import { GoogleCalendarEvent } from "./types/index";
|
||||||
|
|
||||||
|
require("dotenv").config();
|
||||||
|
const env = process.env;
|
||||||
|
|
||||||
|
export default class GoogleCalendar {
|
||||||
|
gamesMap: any = {};
|
||||||
|
clientSecret: string = env.GOOGLE_CLIENT_SECRET;
|
||||||
|
clientId: string = env.GOOGLE_CLIENT_ID;
|
||||||
|
calenderId: string = env.GOOGLE_CALENDAR_ID;
|
||||||
|
calendar: any;
|
||||||
|
clientEmail: string = env.GOOGLE_CLIENT_EMAIL;
|
||||||
|
googlePrivateKey: string = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
|
||||||
|
token: any;
|
||||||
|
JWT_client: JWT;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
console.log("INIT GOOGLE CALENDAR");
|
||||||
|
const jwtClient = await this.authorize();
|
||||||
|
this.calendar = google.calendar({ version: "v3", auth: jwtClient });
|
||||||
|
}
|
||||||
|
|
||||||
|
async authorize() {
|
||||||
|
console.log("AUTHORIZE GOOGLE CALENDAR");
|
||||||
|
this.JWT_client = new JWT({
|
||||||
|
email: this.clientEmail,
|
||||||
|
key: this.googlePrivateKey,
|
||||||
|
scopes: ["https://www.googleapis.com/auth/calendar"],
|
||||||
|
});
|
||||||
|
const { access_token } = await this.JWT_client.authorize();
|
||||||
|
this.token = access_token;
|
||||||
|
if (!this.token) {
|
||||||
|
throw new Error("Failed to connect to google calendar");
|
||||||
|
}
|
||||||
|
console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY");
|
||||||
|
return this.JWT_client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNewEvent(upcomingEvents: GoogleCalendarEvent[]) {
|
||||||
|
// console.log(upcomingEvents)
|
||||||
|
setTimeout(async () => {
|
||||||
|
upcomingEvents.forEach(async (event: GoogleCalendarEvent) => {
|
||||||
|
console.log("UPDATE NEW EVENT", upcomingEvents);
|
||||||
|
const options = {
|
||||||
|
auth: this.JWT_client,
|
||||||
|
calendarId: this.calenderId,
|
||||||
|
resource: {
|
||||||
|
summary: event.summary,
|
||||||
|
location: event.location,
|
||||||
|
description: event.description,
|
||||||
|
start: {
|
||||||
|
dateTime: event.start.dateTime,
|
||||||
|
timeZone: "Asia/Jerusalem",
|
||||||
|
},
|
||||||
|
end: { dateTime: event.end.dateTime, timeZone: "Asia/Jerusalem" },
|
||||||
|
sendNotifications: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await this.calendar.events.insert(
|
||||||
|
options,
|
||||||
|
function (err: any, event: any) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
"There was an error contacting the Calendar service: " + err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(event.description + " created");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isDuplicateEvent(startTime: string, endTime: string, title: string) {
|
||||||
|
if (this.gamesMap[startTime]) {
|
||||||
|
console.log("duplicate event");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.gamesMap[startTime] = true;
|
||||||
|
console.log("checking for duplicate event");
|
||||||
|
try {
|
||||||
|
const response = await this.calendar.events.list({
|
||||||
|
calendarId: this.calenderId,
|
||||||
|
timeMin: startTime,
|
||||||
|
timeMax: endTime,
|
||||||
|
q: title, // Search for events with the same title
|
||||||
|
});
|
||||||
|
return response.data.items.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/Ics.ts
48
src/Ics.ts
|
@ -1,48 +0,0 @@
|
||||||
import { GoogleCalendarEvent } from "./types";
|
|
||||||
import * as ics from 'ics';
|
|
||||||
|
|
||||||
const uuid = require('uuid').v4();
|
|
||||||
|
|
||||||
|
|
||||||
export default class Ics {
|
|
||||||
|
|
||||||
generateIcsOutputFromGames = (games: GoogleCalendarEvent[]) => {
|
|
||||||
let output = [];
|
|
||||||
games.forEach((game) => {
|
|
||||||
output.push(this.generateIcsOutputFromGame(game));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { error, value } = ics.createEvents(output);
|
|
||||||
if (error) {
|
|
||||||
console.log(error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateIcsOutputFromGame = (game: GoogleCalendarEvent) => {
|
|
||||||
const { summary, location, description, start, end } = game;
|
|
||||||
const icsEvent: ics.EventAttributes = {
|
|
||||||
title: summary,
|
|
||||||
description,
|
|
||||||
location,
|
|
||||||
start: [start.dateTime[0], start.dateTime[1], start.dateTime[2], start.dateTime[3], start.dateTime[4]],
|
|
||||||
end: [end.dateTime[0], end.dateTime[1], end.dateTime[2], end.dateTime[3], end.dateTime[4]],
|
|
||||||
url: 'https://mhaifafc.com/',
|
|
||||||
status: 'CONFIRMED',
|
|
||||||
busyStatus: 'BUSY',
|
|
||||||
productId: 'https://mhaifafc.com/',
|
|
||||||
recurrenceRule: '',
|
|
||||||
attendees: [],
|
|
||||||
alarms: [],
|
|
||||||
categories: [],
|
|
||||||
organizer: { name: 'Maccabi Haifa F.C.', email: '' }
|
|
||||||
};
|
|
||||||
return icsEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
convertIcsToIcal = (icsEvents: string) => {
|
|
||||||
const icalEvents = icsEvents.replace(/BEGIN:VEVENT/g, 'BEGIN:VEVENT\r\nUID:' + uuid);
|
|
||||||
return icalEvents;
|
|
||||||
}
|
|
||||||
}
|
|
119
src/index.ts
119
src/index.ts
|
@ -1,85 +1,74 @@
|
||||||
// import GoogleCalendar from './GoogleCalendar';
|
import GameSource from "./GameSource";
|
||||||
import GameSource from './GameSource';
|
import GoogleCalendar from "./GoogleCalendar";
|
||||||
import fs from 'fs';
|
import env from "dotenv";
|
||||||
import Ics from './Ics';
|
import cron from 'node-cron';
|
||||||
import express from 'express'
|
import fs from 'fs'; // Importing fs for logging
|
||||||
import env from 'dotenv';
|
|
||||||
|
|
||||||
env.config();
|
env.config();
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
// googleCalendar: GoogleCalendar;
|
|
||||||
gameSource: GameSource;
|
gameSource: GameSource;
|
||||||
ics: Ics;
|
|
||||||
googleToken: string;
|
googleToken: string;
|
||||||
|
googleCalendar: GoogleCalendar;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// this.googleCalendar = new GoogleCalendar();
|
|
||||||
this.gameSource = new GameSource();
|
this.gameSource = new GameSource();
|
||||||
this.ics = new Ics();
|
this.googleCalendar = new GoogleCalendar();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// async init() {
|
|
||||||
// console.log("INIT APP")
|
|
||||||
// await this.googleCalendar.init();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async getNewGamesAndUpdateCalendar() {
|
|
||||||
// console.log("GET NEW GAMES AND UPDATE CALENDAR")
|
|
||||||
// const games = await this.gameSource.getGamesFromHaifa();
|
|
||||||
// this.googleCalendar.updateNewEvent(games);
|
|
||||||
// }
|
|
||||||
|
|
||||||
async startCronJob() {
|
async startCronJob() {
|
||||||
console.log("START CRON JOB")
|
this.writeLog('START Haifa Reminder'); // Log when the cron job starts
|
||||||
const CronJob = require('cron').CronJob;
|
const newGamesAdded = [];
|
||||||
const job = new CronJob(
|
await this.googleCalendar.init();
|
||||||
"* * * * *", // every day at 10:00,
|
try {
|
||||||
async () => {
|
const games = await this.gameSource.getGamesFromHaifa(this.writeLog);
|
||||||
console.log("Staring a new job")
|
for (const game of games) {
|
||||||
const outputFileLocation = 'public/maccabi-haifa-fc.ics';
|
const isDuplicateEvent = await this.googleCalendar.isDuplicateEvent(
|
||||||
console.log("Getting games from Haifa")
|
game.start.dateTime,
|
||||||
const games = await app.gameSource.getGamesFromHaifa();
|
game.end.dateTime,
|
||||||
console.log("Generating ICS file")
|
game.summary
|
||||||
const icsEvents = app.ics.generateIcsOutputFromGames(games);
|
);
|
||||||
console.log("Writing ICS file to " + outputFileLocation)
|
console.log(game)
|
||||||
fs.writeFileSync(outputFileLocation, icsEvents);
|
if (!isDuplicateEvent) {
|
||||||
console.log("Done Ics file")
|
newGamesAdded.push(game);
|
||||||
|
console.log("Event does not exist");
|
||||||
console.log("converting ics file to ical file")
|
await this.googleCalendar.updateNewEvent([game]);
|
||||||
const outputIcalFileLocation = 'public/maccabi-haifa-fc.ical';
|
} else {
|
||||||
const icalEvents = app.ics.convertIcsToIcal(icsEvents);
|
console.log("Event already exists");
|
||||||
console.log("Writing Ical file to " + outputIcalFileLocation)
|
}
|
||||||
fs.writeFileSync(outputIcalFileLocation, icalEvents);
|
}
|
||||||
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
'Asia/Jerusalem'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (newGamesAdded.length > 0) {
|
||||||
|
console.log("New games added:", newGamesAdded);
|
||||||
|
} else {
|
||||||
|
console.log("No new games were Added!");
|
||||||
|
}
|
||||||
|
this.writeLog('Successfully ran project');
|
||||||
|
} catch (error) {
|
||||||
|
this.writeLog("Error in cron job:" + error.message);
|
||||||
|
} finally {
|
||||||
|
this.writeLog('END CRON JOB'); // Log when the cron job ends
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWebServer() {
|
writeLog(message) {
|
||||||
const webServer = express();
|
const timestamp = new Date().toISOString();
|
||||||
webServer.use(express.static('public'))
|
const logMessage = `${timestamp} - ${message}\n`;
|
||||||
|
// Write to log file synchronously
|
||||||
webServer.listen(process.env.PORT, () => {
|
fs.appendFileSync('cron.log', logMessage);
|
||||||
console.log(`Calender app listening on port ${process.env.PORT}!`)
|
console.log(logMessage); // Optional: also log to console
|
||||||
})
|
|
||||||
|
|
||||||
webServer.use(function (req, res, next) {
|
|
||||||
res.status(404).send("This is not the page you are looking for...")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new App();
|
const app = new App();
|
||||||
|
|
||||||
|
cron.schedule('* * * * *', () => {
|
||||||
app.startCronJob();
|
console.log('Running startCronJob at 10:00 AM Jerusalem time');
|
||||||
app.startWebServer();
|
app.startCronJob().catch((error) => {
|
||||||
|
console.error("Error in scheduled cron job:", error.message);
|
||||||
|
app.writeLog(`ERROR: ${error.message}`); // Log any errors
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: "Asia/Jerusalem"
|
||||||
|
});
|
||||||
|
|
|
@ -3,11 +3,11 @@ export interface GoogleCalendarEvent {
|
||||||
location: string;
|
location: string;
|
||||||
description: string;
|
description: string;
|
||||||
start: {
|
start: {
|
||||||
dateTime: number[];
|
dateTime: string;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
};
|
};
|
||||||
end: {
|
end: {
|
||||||
dateTime: number[];
|
dateTime: string;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
3
start.sh
3
start.sh
|
@ -1,2 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
docker run -p 3000:3000 -d --name haifareminder docker.io/kfda89/haifareminder
|
docker run -p 3000:3000 -d --restart=unless-stopped --name haifareminder docker.io/kfda89/haifareminder
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue