Send prod errors to my chat
[SonOfLokstallBot.git] / src / common.php
1 <?php declare(strict_types=1);
2
3 require_once(__DIR__ . '/config.php');
4 require_once(AUTOLOAD_PATH);
5
6 use Telegram\Bot\Api as TelegramAPI;
7 use Telegram\Bot\Objects\Update as TelegramUpdate;
8
9 final class Monday {
10 private $year;
11 private $month;
12 private $season;
13 private $weekNum;
14 private $dayNum;
15
16 public function __construct(
17 int $year,
18 int $month,
19 string $season,
20 int $weekNum,
21 int $dayNum
22 ) {
23 $this->year = $year;
24 $this->month = $month;
25 $this->season = $season;
26 $this->weekNum = $weekNum;
27 $this->dayNum = $dayNum;
28 }
29
30 public function __get($property) {
31 return $this->$property;
32 }
33 }
34
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');
42 return new Monday(
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)
48 );
49 }
50
51 function getTelegram(): TelegramAPI {
52 STATIC $tg;
53 return $tg = $tg ?? new TelegramAPI(BOT_TOKEN);
54 }
55
56 function splitBill(float $amount) : float {
57 return floor($amount/2);
58 }
59
60 function identity($x) {
61 return $x;
62 }
63
64 const notEmpty = 'notEmpty';
65 function notEmpty($value) : bool {
66 return !empty($value);
67 }
68
69 function getMessageSender(TelegramUpdate $update) : string {
70 return PARTICIPANT_IDS[getMessageSenderId($update)];
71 }
72
73 function getMessageSenderId(TelegramUpdate $update) : int {
74 return $update->get('message')->get('from')->get('id');
75 }
76
77 function getMessageSenderDisplayName(TelegramUpdate $update) : string {
78 return $update->get('message')->get('from')->get('first_name');
79 }
80
81 function canChatWith(TelegramUpdate $update) : bool {
82 return in_array($update->get('message')->get('from')->get('id'), array_keys(PARTICIPANT_IDS));
83 }
84
85 function partition(int $numPartitions, array $array) : array {
86 $partitionSize = (int)ceil(count($array) / $numPartitions);
87
88 return array_values(
89 map(function($p) use ($array, $partitionSize) {
90 return array_slice($array, $p*$partitionSize, $partitionSize);
91 })(range(0, $numPartitions-1))
92 );
93 }
94
95 function getInbox(string $inbox) {
96 STATIC $inboxes;
97
98 if (!isset($inboxes[$inbox])) {
99 $inboxes[$inbox] = imap_open(
100 '{imap.gmail.com:993/debug/imap/ssl/novalidate-cert}' . $inbox,
101 EMAIL,
102 PASSWORD
103 );
104 }
105
106 return $inboxes[$inbox];
107 }
108
109 function getRules() {
110 STATIC $rules;
111 return $rules = $rules ?? require 'rules.php';
112 }
113
114 const getString = 'getString';
115 function getString($identifier, ...$vars) {
116 STATIC $strings;
117 $strings = $strings ?? require 'strings.php';
118
119 return isset($strings[$identifier]) ? sprintf($strings[$identifier], ...$vars) : "[[$identifier]]";
120 }
121
122 const getStringAndCode = 'getStringAndCode';
123 function getStringAndCode($string) {
124 return getString($string) . " (" . $string . ")";
125 };
126
127 function formatDate($date) {
128 return $date->format(DATE_FORMAT);
129 }
130
131 function ssort($comparitor) {
132 return function($array) use ($comparitor) {
133 uasort($array, uncurry($comparitor));
134 return $array;
135 };
136 }
137
138 function uncurry($f) {
139 return function($a, $b) use ($f) {
140 return $f($a)($b);
141 };
142 }
143
144 function between($content, $start){
145 $r = explode($start, $content);
146 if (isset($r[1])){
147 $r = explode($start, $r[1]);
148 return $r[0];
149 }
150 return '';
151 }
152
153 const sendToGroupChat = 'sendToGroupChat';
154 function sendToGroupChat(string $message) {
155 return getTelegram()->sendMessage(['chat_id' => CHAT_ID, 'text' => $message]);
156 }
157
158 const generateReminderText = 'generateReminderText';
159 function generateReminderText($message) {
160 return getString('billreminder', REMIND_THRESHOLD, $message['service'], splitBill($message['amount']), formatDate($message['due']));
161 }
162
163 const generateNewBillText = 'generateNewBillText';
164 function generateNewBillText($message) {
165 return getString('newbill', $message['service'], splitBill($message['amount']), formatDate($message['due']));
166 }
167
168 const messageNeedsReminder = 'messageNeedsReminder';
169 function messageNeedsReminder($message) {
170 return $message['due']->diff(new DateTimeImmutable)->d == REMIND_THRESHOLD;
171 }
172
173 const lines = 'lines';
174 function lines(string $string): array {
175 return explode("\n", $string);
176 }
177
178 const glue = 'glue';
179 function glue(string $delim): callable {
180 return function(array $strings) use ($delim): string {
181 return implode($delim, $strings);
182 };
183 }
184
185 const unlines = 'unlines';
186 function unlines($lines) {
187 return implode("\n", $lines);
188 }
189
190 const ununlines = 'ununlines';
191 function ununlines($lines) {
192 return implode("\n\n", $lines);
193 }
194
195 const zipWith = 'zipWith';
196 function zipWith(callable $zipper, array $a, array $b) {
197 return array_map($zipper, $a, $b);
198 }
199
200 function field($field) {
201 return function($array) use ($field) {
202 return $array[$field];
203 };
204 }
205
206 const ⬄ = '⬄';
207 function ⬄($a) {
208 return function($b) use ($a) {
209 return $a <=> $b;
210 };
211 }
212
213
214 function ∘(...$fs) {
215 return function($arg) use ($fs) {
216 return array_reduce(array_reverse($fs), function($c, $f) {
217 return $f($c);
218 }, $arg);
219 };
220 }
221
222 function map($callable) {
223 return function($list) use ($callable) {
224 return array_map($callable, $list);
225 };
226 }
227
228 function aaray_column($column) {
229 return function($array) use ($column) {
230 return array_column($array, $column);
231 };
232 }
233
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);
238 };
239 };
240 }
241
242 function filter($callable) {
243 return function($list) use ($callable) {
244 return array_filter($list, $callable);
245 };
246 }
247
248 function f∘(callable $f) {
249 return function(callable $g) use ($f) {
250 return function($arg) use($g, $f) {
251 return $f($g($arg));
252 };
253 };
254 }
255
256 function ∘f(callable $f) {
257 return function(callable $g) use ($f) {
258 return function($arg) use($g, $f) {
259 return $g($f($arg));
260 };
261 };
262 }
263
264 function ∪($a, $b) {
265 return array_merge($a, $b);
266 }
267
268 function getSeason(int $monthNum) {
269 return ['summer', 'autumn', 'winter', 'spring'][(int)floor(($monthNum%12)/3)];
270 }
271
272 function getMonthName($monthNum) {
273 return ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][$monthNum-1];
274 }
275
276 function isStartOfSeason($monthNum, $dayNum) {
277 return ($monthNum)%3 == 0 && isStartOfMonth($dayNum);
278 }
279
280 function isStartOfMonth($dayNum) {
281 return $dayNum < 8;
282 }
283
284 function isEndOfSeason($yearNum, $monthNum, $dayNum) {
285 return ($monthNum+1)%3 == 0 && isEndOfMonth($yearNum, $monthNum, $dayNum);
286 }
287
288 function isEndOfMonth($yearNum, $monthNum, $dayNum) {
289 return $dayNum + 7 > cal_days_in_month(CAL_GREGORIAN, $monthNum, $yearNum);
290 }
291
292 function isEndOfYear($yearNum, $monthNum, $dayNum) {
293 return $monthNum == 12 && isEndOfMonth($yearNum, $monthNum, $dayNum);
294 }
295
296 function getTasksForTheSeason($season, $taskMatrix) {
297 return array_unique(
298 array_reduce(
299 $taskMatrix['annualy'][$season],
300 function($c, $v) {
301 return array_merge(
302 $c,
303 array_reduce($v, function($c, $v) {
304 return array_merge($c, is_array($v) ? [] : [$v]);
305 }, [])
306 );
307 },
308 []
309 )
310 );
311 }
312
313 function getTasksForTheMonth($monthNum, $taskMatrix) {
314 return array_merge(
315 $taskMatrix['monthly'],
316 $monthNum % 6 == 0 ? $taskMatrix['biannualy'] : [],
317 $monthNum % 3 == 0 ? $taskMatrix['quadriannualy'] : [],
318 array_filter(
319 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)],
320 function($v) {
321 return !is_array($v);
322 }
323 )
324 );
325 }
326
327 function getTasksForTheWeek(int $year, int $monthNum, int $weekNum, array $taskMatrix) {
328 return array_merge(
329 $weekNum % 2 == 0 ? $taskMatrix['bimonthly'] : [],
330 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)]['weekly'] ?? [],
331 partition(count(getMondaysForMonth($year, $monthNum)), getTasksForTheMonth($monthNum, $taskMatrix))[$weekNum-1]
332 );
333 }
334
335 const getFilePathForWeek = 'getFilePathForWeek';
336 function getFilePathForWeek(int $year, int $monthNum, int $weekNum, string $base) {
337 return sprintf(
338 '%s/tasks/%s/%s/%s/week%s.txt',
339 $base,
340 $year,
341 getSeason($monthNum),
342 getMonthName($monthNum),
343 $weekNum
344 );
345 }
346
347 function getMondaysForMonth(int $year, int $monthNum) {
348 $dt = DateTimeImmutable::createFromFormat('Y n', "$year $monthNum");
349 $m = $dt->format('F');
350 $y = $dt->format('Y');
351 $fifthMonday = (int)(new DateTimeImmutable("fifth monday of $m $y"))->format('d');
352 $mondays = [
353 (int)(new DateTimeImmutable("first monday of $m $y"))->format('d'),
354 (int)(new DateTimeImmutable("second monday of $m $y"))->format('d'),
355 (int)(new DateTimeImmutable("third monday of $m $y"))->format('d'),
356 (int)(new DateTimeImmutable("fourth monday of $m $y"))->format('d'),
357 ];
358
359 return array_merge(
360 $mondays,
361 $fifthMonday > $mondays[3] ? [$fifthMonday] : []
362 );
363 }
364
365 function getDayNumber(int $year, int $month, int $day) {
366 $potentialMondays = array_filter(
367 getMondaysForMonth($year, $month),
368 function($monday) use ($day) {
369 return $monday <= $day;
370 }
371 );
372
373 return $potentialMondays
374 ? closest($day, $potentialMondays)
375 : array_values(
376 array_slice(
377 getMondaysForMonth(
378 getYearWeekBeginsIn($year, $month, $day),
379 getMonthWeekBeginsIn($year, $month, $day)
380 ),
381 -1
382 )
383 )[0];
384 }
385
386 function getWeekNumber(int $year, int $month, int $day) {
387 $potentialMondays = array_filter(
388 getMondaysForMonth($year, $month),
389 function($monday) use ($day) {
390 return $monday <= $day;
391 }
392 );
393
394 return $potentialMondays
395 ? closestIndex($day, $potentialMondays) + 1
396 : count(
397 getMondaysForMonth(
398 getYearWeekBeginsIn($year, $month, $day),
399 getMonthWeekBeginsIn($year, $month, $day)
400 )
401 );
402 }
403
404 function getMonthWeekBeginsIn(int $year, int $month, int $day) {
405 return array_merge([12], range(1,11), [12])[
406 $month - ($day < getMondaysForMonth($year, $month)[0] ? 1 : 0)
407 ];
408 }
409
410 function getYearWeekBeginsIn(int $year, int $month, int $day) {
411 return $year - (int)($month == 1 && getMonthWeekBeginsIn($year, $month, $day) == 12);
412 }
413
414 function getFilePathsForMonth(int $year, int $monthNum, string $base) {
415 return map(function($week) use ($year, $monthNum, $base){
416 return getFilePathForWeek($year, $monthNum, $week, $base);
417 })(range(1, count(getMondaysForMonth($year, $monthNum))));
418 }
419
420 function getFilePathsForSeason(int $year, string $season, string $base) {
421 return array_merge(...map(function($monthNum) use ($year, $base) {
422 // Summer of the current year includes december of the previous year.
423 $seasonYear = $year - (int)($monthNum == 12);
424 return getFilePathsForMonth($seasonYear, $monthNum, $base);
425 })(array_filter(range(1,12), function($month) use ($season) {
426 return getSeason($month) == $season;
427 })));
428 }
429
430 function getFilePathsForYear(int $year, string $base) {
431 return array_merge(...map(function($season) use ($year, $base) {
432 return getFilePathsForSeason($year, $season, $base);
433 })(['summer', 'winter', 'spring', 'autumn']));
434 }
435
436 function closestIndex(int $n, array $list) {
437 $a = map(function(int $v) use ($n) : int {
438 return abs($v - $n);
439 })
440 ($list);
441
442 asort($a);
443 return array_keys($a)[0];
444 }
445
446 function closest(int $n, array $list) : int {
447 return $list[closestIndex($n, $list)];
448 }
449
450 function reveal($str) {
451 $lastBytes = array_filter(
452 (unpack('C*', $str)),
453 function($key) {
454 return $key % 4 == 0;
455 },
456 ARRAY_FILTER_USE_KEY
457 );
458
459 $penultimateBytes = array_filter(
460 (unpack('C*', $str)),
461 function($key) {
462 return ($key + 1) % 4 ==
463 0;
464 },
465 ARRAY_FILTER_USE_KEY
466 );
467
468 return implode('', zipWith(function($byte, $prevByte) {
469 return chr($byte - ($prevByte == 133 ? 64 : 128));
470 }, $lastBytes, $penultimateBytes));
471 }
472
473 function hide($str) {
474 $charBytes = unpack('C*', $str);
475 return pack(...array_merge(['C*'], array_merge(...map(function($charByte) {
476 $magic = [243, 160, $charByte < 65 ? 132 : 133];
477 return array_merge($magic, [$charByte + ($charByte < 65 ? 128 : 64)]);
478 })($charBytes))));
479 }
480
481 function getMessagesFromInbox($inbox, array $rules, $unseenOnly = true) {
482 return array_filter(
483 array_map(
484 function($rule, $service) use ($inbox, $unseenOnly) {
485 $emails = imap_search($inbox, ['SEEN ', 'UNSEEN '][$unseenOnly] . $rule['imapQuery'], SE_UID);
486
487 if(!$emails) {
488 return [];
489 }
490
491 $messageTransform = $rule['messageTransform'] ?? 'identity';
492 $dateTransform = $rule['dateTransform'] ?? 'identity';
493
494 $body = quoted_printable_decode(imap_fetchbody($inbox, $emails[0], '1', FT_UID));
495 preg_match($rule['regex'], $messageTransform($body), $matches);
496
497 return [
498 'service' => $service,
499 'id' => substr(md5($body), 0, 6),
500 'uid' => $emails[0],
501 'due' => new DateTimeImmutable($dateTransform($matches['due'])),
502 'amount' => (float)$matches['amount']
503 ];
504 },
505 $rules,
506 array_keys($rules)
507 ),
508 function($e) {
509 return !!$e;
510 }
511 );
512 }
513
514 function log_error($num, $str, $file, $line, $context = null) {
515 $message = $str . "\n" . $file . "(" . $line . ")";
516 getTelegram()->sendMessage([
517 'chat_id' => array_flip(PARTICIPANT_IDS)['Cam'],
518 'parse_mode' => 'Markdown',
519 '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```"
520 ]);
521 }
522
523 function log_exception(Throwable $e) {
524 $message = $e->getMessage() . "\n" . $e->getFile() . "(" . $e->getLine() . ")";
525 getTelegram()->sendMessage([
526 'chat_id' => array_flip(PARTICIPANT_IDS)['Cam'],
527 'parse_mode' => 'Markdown',
528 '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```"
529 ]);
530 exit();
531 }
532
533 function check_for_fatal() {
534 $error = error_get_last();
535 if (in_array(
536 $error['type'],
537 [E_PARSE, E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]
538 )) {
539 $message = $error['message'] . "\n" . $error['file'] . "(" . $error['line'] . ")";
540 getTelegram()->sendMessage([
541 'chat_id' => array_flip(PARTICIPANT_IDS)['Cam'],
542 'parse_mode' => 'Markdown',
543 '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```"
544 ]);
545 exit();
546 }
547
548 if($error) {
549 $message = $error['message'] . "\n" . $error['file'] . "(" . $error['line'] . ")";
550 getTelegram()->sendMessage([
551 'chat_id' => array_flip(PARTICIPANT_IDS)['Cam'],
552 'parse_mode' => 'Markdown',
553 '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```"
554 ]);
555 }
556 }
557
558 if (SEND_ERRORS_TO_TELEGRAM) {
559 register_shutdown_function( "check_for_fatal" );
560 set_error_handler( "log_error" );
561 set_exception_handler( "log_exception" );
562 error_reporting( E_ALL );
563 }