Kaktus dobíječka avatar
Kaktus dobíječka

Pricing

Pay per usage

Go to Store
Kaktus dobíječka

Kaktus dobíječka

Developed by

Jan

Jan

Maintained by Community

Actor which checks date of top up double action for Czech phone operator Kaktus. It's possible to set e-mail for sending notification if the day is today. Fixed changes on website in 2025. Time is scraped only when it's available on Kaktus website.

0.0 (0)

Pricing

Pay per usage

2

Total users

4

Monthly users

3

Runs succeeded

>99%

Last modified

5 days ago

.dockerignore

# configurations
.idea
# crawlee and apify storage folders
apify_storage
crawlee_storage
storage
# installed files
node_modules
# git folder
.git

.editorconfig

root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

.eslintrc

{
"extends": "@apify",
"root": true,
"rules": {
"padded-blocks": "off",
"indent": [
"warn",
2
],
"eol-last": "off",
"max-classes-per-file": "off",
"radix": "off",
"no-undef": "off",
"import/extensions": "off",
"max-len": "off",
"no-trailing-spaces": "off",
"object-shorthand": "off"
}
}

.gitignore

# This file tells Git which files shouldn't be added to source control
.idea
dist
node_modules
apify_storage
storage
apify_storage

apify.json.deprecated

{
"name": "kaktus-dobijecka",
"version": "0.0",
"buildTag": "latest",
"env": null,
"template": "project_cheerio_crawler_js"
}

Dockerfile

# Specify the base Docker image. You can read more about
# the available images at https://sdk.apify.com/docs/guides/docker-images
# You can also use any other image from Docker Hub.
FROM apify/actor-node:16
# Copy just package.json and package-lock.json
# to speed up the build using Docker layer cache.
COPY package*.json ./
# Install NPM packages, skip optional and development dependencies to
# keep the image small. Avoid logging too much and print the dependency
# tree for debugging
RUN npm --quiet set progress=false \
&& npm install --omit=dev --omit=optional \
&& echo "Installed NPM packages:" \
&& (npm list --omit=dev --all || true) \
&& echo "Node.js version:" \
&& node --version \
&& echo "NPM version:" \
&& npm --version \
&& rm -r ~/.npm
# Next, copy the remaining files and directories with the source code.
# Since we do this after NPM install, quick build will be really fast
# for most source file changes.
COPY . ./
# Run the image.
CMD npm start --silent

INPUT_SCHEMA.json

{
"title": "CheerioCrawler Template",
"type": "object",
"schemaVersion": 1,
"properties": {
"email": {
"title": "E-mail data",
"type": "array",
"description": "Set to, cc and bcc. You can set more objects with addresses, in this case more e-mails will be sent.",
"prefill": [{"to": "your@email.com"}],
"editor": "json"
}
}
}

package.json

{
"name": "kaktus-dobijecka",
"version": "0.0.1",
"type": "module",
"description": "This is a boilerplate of an Apify actor.",
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"apify": "^3.0.0",
"crawlee": "^3.0.0"
},
"devDependencies": {
"@apify/eslint-config": "^0.3.1",
"chai": "^4.3.6",
"eslint": "^8.20.0",
"mocha": "^10.1.0"
},
"scripts": {
"start": "node src/main.js",
"lint": "eslint ./src --ext .js,.jsx",
"lint:fix": "eslint ./src --ext .js,.jsx --fix",
"test": "mocha --recursive"
},
"author": "It's not you it's me",
"license": "ISC"
}

.actor/actor.json

{
"actorSpecification": 1,
"name": "kaktus-dobijecka",
"version": "0.0",
"buildTag": "latest"
}

test/test.js

1import { assert } from 'chai';
2
3import { Utils, Validity } from '../src/utils.js';
4
5describe('isSameDay', () => {
6
7 it('should should return true for same days', () => {
8 assert.isTrue(Utils.isSameDay(new Date(), new Date()));
9 assert.isTrue(Utils.isSameDay(new Date(2022, 9, 23), new Date(2022, 9, 23, 15, 10, 0)));
10 });
11
12 it('should should return false for different days', () => {
13 assert.isFalse(Utils.isSameDay(new Date(2022, 10, 23), new Date(2022, 9, 23)));
14 });
15
16});
17
18describe('parseDate', () => {
19
20 it('should parse date from string', () => {
21 assert.deepEqual(Utils.parseDate('https://www.mujkaktus.cz/api/download?docUrl=%2Fapi%2Fdocuments%2Ffile%2FOP-Odmena-za-dobiti-FB_23062025.pdf&filename=OP-Odmena-za-dobiti-FB_23062025.pdf'), new Validity(new Date(2025, 5, 23)));
22 });
23
24 it('should not parse date from string', () => {
25 assert.isNull(Utils.parseDate('https://sluzby.mujkaktus.cz/moje-sluzby'));
26 });
27});
28
29describe('parseDateTimeFromText', () => {
30
31 it('should parse date and time from text', () => {
32
33 const text = `
34 <div class="richTextStyles"><h4><strong>9.7.2025 16:00 - 18:00</strong></h4><p><br></p><p><strong>Jak dobít kredit? </strong><br>Nejrychlejší to máš online <a href="/chces-pridat#dobiti">tady dole</a> a v apce nebo sámošce platební kartou. Jde to i přes Sazku, bankomat, internetové bankovnictví a prodejnu T-Mobile. Když nenajdeš Kaktus, zvol T-Mobile.<br><br><strong>Jak získat bonus? </strong><br>Dobij si během akce aspoň 200 Kč a dostaneš jednou tolik navíc. Získáš ho jen jednou za akci a maximálně 500 Kč. Přeposlání kreditu z jiného čísla není dobití. <br><br><strong>Jak bonusový kredit funguje?</strong><br>Můžeš ho použít na nákup balíčků, datování, volání, posílání SMS/MMS a čerpá se jako první. Nejde přeposílat na jiná čísla ani jím platit Premium SMS, Audiotex ani M-Platba. Má platnost 30 dní a kontrolovat ho můžeš v <a href="http://kaktus.gods.cz/aplikace/d/premium1">apce</a>, <a href="https://sluzby.mujkaktus.cz/moje-sluzby">sámošce</a> nebo vytočením *103#. <br><br><a href="https://www.mujkaktus.cz/api/download?docUrl=%2Fapi%2Fdocuments%2Ffile%2FOP-Odmena-za-dobiti-FB_09072025.pdf&amp;filename=OP-Odmena-za-dobiti-FB_09072025.pdf" rel="noopener noreferrer" target="_blank">Celé podmínky v PDF</a></p></div>
35 `;
36
37 assert.deepEqual(Utils.parseDateTimeFromText(text), {
38 date: new Date(2025, 6, 9, 16, 0),
39 startTime: '16:00',
40 endTime: '18:00',
41 });
42 });
43
44 it('should not parse date and time from text', () => {
45 assert.isNull(Utils.parseDateTimeFromText(''));
46 });
47});

src/main.js

1import { Actor } from 'apify';
2import { CheerioCrawler } from 'crawlee';
3import { router } from './routes.js';
4
5await Actor.init();
6
7const startUrls = ['https://www.mujkaktus.cz/chces-pridat'];
8
9const crawler = new CheerioCrawler({
10 requestHandler: router,
11});
12
13await crawler.run(startUrls);
14
15await Actor.exit();

src/routes.js

1import { createCheerioRouter } from 'crawlee';
2import { Utils } from './utils.js';
3
4export const router = createCheerioRouter();
5
6router.addDefaultHandler(async ({ $ }) => {
7
8 // let's try to parse date and time from text
9 const textResult =
10 Utils.parseDateTimeFromText($('div.richTextStyles').text());
11
12 if (textResult) {
13 await Utils.handleResult(textResult);
14 return;
15 }
16
17 // if not, let's try to parse date from PDF terms filename
18 for (const a of $('a')) {
19
20 if (!$(a).attr('href')?.startsWith('https://www.mujkaktus.cz/api/download')
21 || !$(a).attr('href')?.endsWith('.pdf')) {
22 continue;
23 }
24
25 const text = $(a).attr('href');
26
27 const validity = Utils.parseDate(text);
28
29 if (validity) {
30 await Utils.handleResult(validity);
31 }
32 }
33});

src/utils.js

1import { Actor } from 'apify';
2
3export class Utils {
4
5 static parseDate(input) {
6
7 // First try to match date in format DDMMYYYY from filename (e.g., 23062025)
8 const filenameMatch = input.match(/(\d{2})(\d{2})(\d{4})\.pdf/);
9 if (filenameMatch?.length === 4) {
10 const day = parseInt(filenameMatch[1]);
11 const month = parseInt(filenameMatch[2]);
12 const year = parseInt(filenameMatch[3]);
13
14 const parsedDate = new Date(year, month - 1, day);
15 return new Validity(parsedDate);
16 }
17
18 return null;
19 }
20
21 static isSameDay(a, b) {
22 return a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
23 }
24
25 static getResult(validity) {
26 const { date } = validity;
27
28 var result = {
29 Date: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`,
30 From: validity.startTime,
31 To: validity.endTime,
32 };
33
34 return result;
35 }
36
37 static parseDateTimeFromText(text) {
38 // Parse date and time from HTML text like "9.7.2025 16:00 - 18:00"
39
40 // Match pattern: DD.MM.YYYY HH:MM - HH:MM
41 const dateTimeMatch = text.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})\s+(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})/);
42
43 if (dateTimeMatch?.length === 8) {
44 const day = parseInt(dateTimeMatch[1]);
45 const month = parseInt(dateTimeMatch[2]);
46 const year = parseInt(dateTimeMatch[3]);
47 const startHour = parseInt(dateTimeMatch[4]);
48 const startMinute = parseInt(dateTimeMatch[5]);
49 const endHour = parseInt(dateTimeMatch[6]);
50 const endMinute = parseInt(dateTimeMatch[7]);
51
52 // Create start and end date objects
53 const startDate = new Date(year, month - 1, day, startHour, startMinute);
54
55 return {
56 date: startDate, // For compatibility with existing code
57 startTime: `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}`,
58 endTime: `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`
59 };
60 }
61
62 return null;
63 }
64
65 static async handleResult(result) {
66 await Actor.pushData(Utils.getResult(result));
67
68 if (Utils.isSameDay(result.date, new Date())) {
69
70 const { email: emailsData } = await Actor.getInput();
71
72 if (emailsData) {
73 for (const emailData of emailsData) {
74 await Utils.sendEmail(emailData, result);
75 }
76 }
77
78 }
79 }
80
81 static async sendEmail(emailData, result) {
82
83 let subject = `Kaktus dobíječka dnes`;
84
85 if (result.startTime && result.endTime) {
86 subject = `${subject} ${result.startTime} - ${result.endTime}`;
87 }
88
89 await Actor.call('apify/send-mail', {
90 to: emailData.to,
91 cc: emailData.cc,
92 bcc: emailData.bcc,
93 subject: subject,
94 html: 'Podívat se na <a href="https://www.mujkaktus.cz/chces-pridat">web</a>',
95 });
96
97 }
98
99}
100
101export class Validity {
102 constructor(date) {
103 this.date = date;
104 }
105
106}