1 <?php
declare(strict_types
=1);
3 require_once(__DIR__
. '/config.php');
4 require_once(AUTOLOAD_PATH
);
6 use Telegram\Bot\Api
as TelegramAPI
;
7 use Telegram\Bot\Objects\Update
as TelegramUpdate
;
16 public function __construct(
24 $this->month
= $month;
25 $this->season
= $season;
26 $this->weekNum
= $weekNum;
27 $this->dayNum
= $dayNum;
30 public function __get($property) {
31 return $this->$property;
35 // Returns a the closest previous Monday given a date.
36 // weekNumber is 1-5 (mondays in months are considered the start of a week, so if a month has 5 mondays it has 5 weeks)
37 // dayNumber is the day of the month
38 function turnBackTime(DateTimeImmutable
$date) {
39 $y = (int) $date->format('Y');
40 $m = (int) $date->format('n');
41 $d = (int) $date->format('d');
43 getYearWeekBeginsIn($y, $m, $d),
44 getMonthWeekBeginsIn($y, $m, $d),
45 getSeason(getMonthWeekBeginsIn($y, $m, $d)),
46 getWeekNumber($y, $m, $d),
47 getDayNumber($y, $m, $d)
51 function getTelegram(): TelegramAPI
{
53 return $tg = $tg ??
new TelegramAPI(BOT_TOKEN
);
56 function splitBill(float $amount) : float {
60 function identity($x) {
64 const notEmpty
= 'notEmpty';
65 function notEmpty($value) : bool
{
66 return !empty($value);
69 function getMessageSender(TelegramUpdate
$update) : string {
70 return PARTICIPANT_IDS
[getMessageSenderId($update)];
73 function getMessageSenderId(TelegramUpdate
$update) : int {
74 return $update->get('message')->get('from')->get('id');
77 function getMessageSenderDisplayName(TelegramUpdate
$update) : string {
78 return $update->get('message')->get('from')->get('first_name');
81 function canChatWith(TelegramUpdate
$update) : bool
{
82 return in_array($update->get('message')->get('from')->get('id'), array_keys(PARTICIPANT_IDS
));
85 function partition(int $numPartitions, array $array) : array {
86 $partitionSize = (int)ceil(count($array) / $numPartitions);
89 map(function($p) use ($array, $partitionSize) {
90 return array_slice($array, $p*$partitionSize, $partitionSize);
91 })(range(0, $numPartitions-1))
95 function getInbox(string $inbox) {
98 if (!isset($inboxes[$inbox])) {
99 $inboxes[$inbox] = imap_open(
100 '{imap.gmail.com:993/debug/imap/ssl/novalidate-cert}' . $inbox,
106 return $inboxes[$inbox];
109 function getRules() {
111 return $rules = $rules ??
require 'rules.php';
114 const getString
= 'getString';
115 function getString($identifier, ...$vars) {
117 $strings = $strings ??
require 'strings.php';
119 return isset($strings[$identifier]) ?
sprintf($strings[$identifier], ...$vars) : "[[$identifier]]";
122 const getStringAndCode
= 'getStringAndCode';
123 function getStringAndCode($string) {
124 return getString($string) . " (" . $string . ")";
127 function formatDate($date) {
128 return $date->format(DATE_FORMAT
);
131 function ssort($comparitor) {
132 return function($array) use ($comparitor) {
133 uasort($array, uncurry($comparitor));
138 function uncurry($f) {
139 return function($a, $b) use ($f) {
144 function between($content, $start){
145 $r = explode($start, $content);
147 $r = explode($start, $r[1]);
153 const sendToGroupChat
= 'sendToGroupChat';
154 function sendToGroupChat(string $message) {
155 return getTelegram()->sendMessage(['chat_id' => CHAT_ID
, 'text' => $message]);
158 const generateReminderText
= 'generateReminderText';
159 function generateReminderText($message) {
160 return getString('billreminder', REMIND_THRESHOLD
, $message['service'], splitBill($message['amount']), formatDate($message['due']));
163 const generateNewBillText
= 'generateNewBillText';
164 function generateNewBillText($message) {
165 return getString('newbill', $message['service'], splitBill($message['amount']), formatDate($message['due']));
168 const messageNeedsReminder
= 'messageNeedsReminder';
169 function messageNeedsReminder($message) {
170 return $message['due']->diff(new DateTimeImmutable
)->d
== REMIND_THRESHOLD
;
173 const lines
= 'lines';
174 function lines(string $string): array {
175 return explode("\n", $string);
179 function glue(string $delim): callable
{
180 return function(array $strings) use ($delim): string {
181 return implode($delim, $strings);
185 const unlines
= 'unlines';
186 function unlines($lines) {
187 return implode("\n", $lines);
190 const ununlines
= 'ununlines';
191 function ununlines($lines) {
192 return implode("\n\n", $lines);
195 const zipWith
= 'zipWith';
196 function zipWith(callable
$zipper, array $a, array $b) {
197 return array_map($zipper, $a, $b);
200 function field($field) {
201 return function($array) use ($field) {
202 return $array[$field];
208 return function($b) use ($a) {
214 function ∘
(...$fs) {
215 return function($arg) use ($fs) {
216 return array_reduce(array_reverse($fs), function($c, $f) {
222 function map($callable) {
223 return function($list) use ($callable) {
224 return array_map($callable, $list);
228 function aaray_column($column) {
229 return function($array) use ($column) {
230 return array_column($array, $column);
234 function aaray_slice($start) {
235 return function($length) use ($start) {
236 return function($array) use ($length, $start) {
237 return array_slice($array, $start, $length);
242 function filter($callable) {
243 return function($list) use ($callable) {
244 return array_filter($list, $callable);
248 function f∘
(callable
$f) {
249 return function(callable
$g) use ($f) {
250 return function($arg) use($g, $f) {
256 function ∘
f(callable
$f) {
257 return function(callable
$g) use ($f) {
258 return function($arg) use($g, $f) {
264 function ∪
($a, $b) {
265 return array_merge($a, $b);
268 function getSeason(int $monthNum) {
269 return ['summer', 'autumn', 'winter', 'spring'][(int)floor(($monthNum%12
)/3)];
272 const getMonthName
= 'getMonthName';
273 function getMonthName($monthNum) {
274 return ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][$monthNum-1];
277 function isStartOfSeason($monthNum, $dayNum) {
278 return ($monthNum)%3
== 0 && isStartOfMonth($dayNum);
281 function isStartOfMonth($dayNum) {
285 function isEndOfSeason($yearNum, $monthNum, $dayNum) {
286 return ($monthNum+
1)%3
== 0 && isEndOfMonth($yearNum, $monthNum, $dayNum);
289 function isEndOfMonth($yearNum, $monthNum, $dayNum) {
290 return $dayNum +
7 > cal_days_in_month(CAL_GREGORIAN
, $monthNum, $yearNum);
293 function isEndOfYear($yearNum, $monthNum, $dayNum) {
294 return $monthNum == 12 && isEndOfMonth($yearNum, $monthNum, $dayNum);
297 function getTasksForTheSeason($season, $taskMatrix) {
300 $taskMatrix['annualy'][$season],
304 array_reduce($v, function($c, $v) {
305 return array_merge($c, is_array($v) ?
[] : [$v]);
314 function getTasksForTheMonth($monthNum, $taskMatrix) {
316 $taskMatrix['monthly'],
317 $monthNum %
6 == 0 ?
$taskMatrix['biannualy'] : [],
318 $monthNum %
3 == 0 ?
$taskMatrix['quadriannualy'] : [],
320 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)],
322 return !is_array($v);
328 function getTasksForTheWeek(int $year, int $monthNum, int $weekNum, array $taskMatrix) {
330 $weekNum %
2 == 0 ?
$taskMatrix['bimonthly'] : [],
331 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)]['weekly'] ??
[],
332 partition(count(getMondaysForMonth($year, $monthNum)), getTasksForTheMonth($monthNum, $taskMatrix))[$weekNum-1]
336 const getFilePathForWeek
= 'getFilePathForWeek';
337 function getFilePathForWeek(int $year, int $monthNum, int $weekNum, string $base) {
339 '%s/tasks/%s/%s/%s/week%s.txt',
342 getSeason($monthNum),
343 getMonthName($monthNum),
348 function getMondaysForMonth(int $year, int $monthNum) {
349 $dt = DateTimeImmutable
::createFromFormat('Y n', "$year $monthNum");
350 $m = $dt->format('F');
351 $y = $dt->format('Y');
352 $fifthMonday = (int)(new DateTimeImmutable("fifth monday of $m $y"))->format('d');
354 (int)(new DateTimeImmutable("first monday of $m $y"))->format('d'),
355 (int)(new DateTimeImmutable("second monday of $m $y"))->format('d'),
356 (int)(new DateTimeImmutable("third monday of $m $y"))->format('d'),
357 (int)(new DateTimeImmutable("fourth monday of $m $y"))->format('d'),
362 $fifthMonday > $mondays[3] ?
[$fifthMonday] : []
366 function getDayNumber(int $year, int $month, int $day) {
367 $potentialMondays = array_filter(
368 getMondaysForMonth($year, $month),
369 function($monday) use ($day) {
370 return $monday <= $day;
374 return $potentialMondays
375 ?
closest($day, $potentialMondays)
379 getYearWeekBeginsIn($year, $month, $day),
380 getMonthWeekBeginsIn($year, $month, $day)
387 function getWeekNumber(int $year, int $month, int $day) {
388 $potentialMondays = array_filter(
389 getMondaysForMonth($year, $month),
390 function($monday) use ($day) {
391 return $monday <= $day;
395 return $potentialMondays
396 ?
closestIndex($day, $potentialMondays) +
1
399 getYearWeekBeginsIn($year, $month, $day),
400 getMonthWeekBeginsIn($year, $month, $day)
405 function getMonthWeekBeginsIn(int $year, int $month, int $day) {
406 return array_merge([12], range(1,11), [12])[
407 $month - ($day < getMondaysForMonth($year, $month)[0] ?
1 : 0)
411 function getYearWeekBeginsIn(int $year, int $month, int $day) {
412 return $year - (int)($month == 1 && getMonthWeekBeginsIn($year, $month, $day) == 12);
415 function getFilePathsForMonth(int $year, int $monthNum, string $base) {
416 return map(function($week) use ($year, $monthNum, $base){
417 return getFilePathForWeek($year, $monthNum, $week, $base);
418 })(range(1, count(getMondaysForMonth($year, $monthNum))));
421 function getFilePathsForSeason(int $year, string $season, string $base) {
422 return array_merge(...map(function($monthNum) use ($year, $base) {
423 // Summer of the current year includes december of the previous year.
424 $seasonYear = $year - (int)($monthNum == 12);
425 return getFilePathsForMonth($seasonYear, $monthNum, $base);
426 })(array_filter(range(1,12), function($month) use ($season) {
427 return getSeason($month) == $season;
431 function getFilePathsForYear(int $year, string $base) {
432 return array_merge(...map(function($season) use ($year, $base) {
433 return getFilePathsForSeason($year, $season, $base);
434 })(['summer', 'winter', 'spring', 'autumn']));
437 function closestIndex(int $n, array $list) {
438 $a = map(function(int $v) use ($n) : int {
444 return array_keys($a)[0];
447 function closest(int $n, array $list) : int {
448 return $list[closestIndex($n, $list)];
451 function reveal($str) {
452 $lastBytes = array_filter(
453 (unpack('C*', $str)),
455 return $key %
4 == 0;
460 $penultimateBytes = array_filter(
461 (unpack('C*', $str)),
463 return ($key +
1) %
4 ==
469 return implode('', zipWith(function($byte, $prevByte) {
470 return chr($byte - ($prevByte == 133 ?
64 : 128));
471 }, $lastBytes, $penultimateBytes));
474 function hide($str) {
475 $charBytes = unpack('C*', $str);
476 return pack(...array_merge(['C*'], array_merge(...map(function($charByte) {
477 $magic = [243, 160, $charByte < 65 ?
132 : 133];
478 return array_merge($magic, [$charByte +
($charByte < 65 ?
128 : 64)]);
482 function getMessagesFromInbox($inbox, array $rules, $unseenOnly = true
) {
485 function($rule, $service) use ($inbox, $unseenOnly) {
486 $emails = imap_search($inbox, ['SEEN ', 'UNSEEN '][$unseenOnly] . $rule['imapQuery'], SE_UID
);
492 $messageTransform = $rule['messageTransform'] ??
'identity';
493 $dateTransform = $rule['dateTransform'] ??
'identity';
495 $body = quoted_printable_decode(imap_fetchbody($inbox, $emails[0], '1', FT_UID
));
496 preg_match($rule['regex'], $messageTransform($body), $matches);
499 'service' => $service,
500 'id' => substr(md5($body), 0, 6),
502 'due' => new DateTimeImmutable($dateTransform($matches['due'])),
503 'amount' => (float)$matches['amount']
515 function log_error($num, $str, $file, $line, $context = null
) {
516 $message = $str . "\n" . $file . "(" . $line . ")";
517 getTelegram()->sendMessage([
518 'chat_id' => array_flip(PARTICIPANT_IDS
)['Cam'],
519 'parse_mode' => 'Markdown',
520 'text' => "I think there's a mistake in my programming. I can still run but something went wrong... Here's what I know:\n\n```\n" . $message . "\n```"
524 function log_exception(Throwable
$e) {
525 $message = $e->getMessage() . "\n" . $e->getFile() . "(" . $e->getLine() . ")";
526 getTelegram()->sendMessage([
527 'chat_id' => array_flip(PARTICIPANT_IDS
)['Cam'],
528 'parse_mode' => 'Markdown',
529 'text' => "Oh man, I hurt myself trying to do something and had to stop... Here's what I know:\n\n```\n" . $message . "\n\n" . $e->getTraceAsString() . "\n```"
534 function check_for_fatal() {
535 $error = error_get_last();
538 [E_PARSE
, E_ERROR
, E_CORE_ERROR
, E_COMPILE_ERROR
, E_USER_ERROR
]
540 $message = $error['message'] . "\n" . $error['file'] . "(" . $error['line'] . ")";
541 getTelegram()->sendMessage([
542 'chat_id' => array_flip(PARTICIPANT_IDS
)['Cam'],
543 'parse_mode' => 'Markdown',
544 'text' => "Oh man, I hurt myself trying to do something and had to stop... Here's what I know:\n\n```\n" . $message . "\n\n" . $e->getTraceAsString() . "\n```"
550 $message = $error['message'] . "\n" . $error['file'] . "(" . $error['line'] . ")";
551 getTelegram()->sendMessage([
552 'chat_id' => array_flip(PARTICIPANT_IDS
)['Cam'],
553 'parse_mode' => 'Markdown',
554 'text' => "I think there's a mistake in my programming. I can still run but something went wrong... Here's what I know:\n\n```\n" . $message . "\n```"
559 if (SEND_ERRORS_TO_TELEGRAM
) {
560 register_shutdown_function( "check_for_fatal" );
561 set_error_handler( "log_error" );
562 set_exception_handler( "log_exception" );
563 error_reporting( E_ALL
);