get('message')->get('from')->get('id'); } function getMessageSenderDisplayName($update) { return $update->get('message')->get('from')->get('first_name'); } function canChatWith($update) { return in_array($update->get('message')->get('from')->get('id'), array_keys(PARTICIPANT_IDS)); } function debug($whatever) { echo '
';
    print_r($whatever);
    echo '
'; } function partition(int $numPartitions, $array) { $partitionSize = (int)ceil(count($array) / $numPartitions); return array_values(filter(notEmpty)( map(function($p) use ($array, $partitionSize) { return array_slice($array, $p*$partitionSize, $partitionSize); })(range(0, $numPartitions-1)) )); } function getInbox($inbox) { STATIC $inboxes; if (!isset($inboxes[$inbox])) { $inboxes[$inbox] = imap_open( '{imap.gmail.com:993/debug/imap/ssl/novalidate-cert}' . $inbox, EMAIL, PASSWORD ); } return $inboxes[$inbox]; } function getRules() { STATIC $rules; return $rules = $rules ?? require 'rules.php'; } const getString = 'getString'; function getString($identifier, ...$vars) { STATIC $strings; $strings = $strings ?? require 'strings.php'; return isset($strings[$identifier]) ? sprintf($strings[$identifier], ...$vars) : "[[$identifier]]"; } const getStringAndCode = 'getStringAndCode'; function getStringAndCode($string) { return getString($string) . " (" . $string . ")"; }; function formatDate($date) { return $date->format(DATE_FORMAT); } function ssort($comparitor) { return function($array) use ($comparitor) { uasort($array, uncurry($comparitor)); return $array; }; } function uncurry($f) { return function($a, $b) use ($f) { return $f($a)($b); }; } const sendToGroupChat = 'sendToGroupChat'; function sendToGroupChat(string $message) { return getTelegram()->sendMessage(['chat_id' => CHAT_ID, 'text' => $message]); } const generateReminderText = 'generateReminderText'; function generateReminderText($message) { return getString('billreminder', REMIND_THRESHOLD, $message['service'], splitBill($message['amount']), formatDate($message['due'])); } const generateNewBillText = 'generateNewBillText'; function generateNewBillText($message) { return getString('newbill', $message['service'], splitBill($message['amount']), formatDate($message['due'])); } const messageNeedsReminder = 'messageNeedsReminder'; function messageNeedsReminder($message) { return $message['due']->diff(new DateTimeImmutable)->d == REMIND_THRESHOLD; } const lines = 'lines'; function lines(string $string): array { return explode("\n", $string); } const glue = 'glue'; function glue(string $delim): callable { return function(array $strings) use ($delim): string { return implode($delim, $strings); }; } const unlines = 'unlines'; function unlines($lines) { return implode("\n", $lines); } const ununlines = 'ununlines'; function ununlines($lines) { return implode("\n\n", $lines); } const zipWith = 'zipWith'; function zipWith(callable $zipper, array $a, array $b) { return array_map($zipper, $a, $b); } function field($field) { return function($array) use ($field) { return $array[$field]; }; } const ⬄ = '⬄'; function ⬄($a) { return function($b) use ($a) { return $a <=> $b; }; } function ∘(...$fs) { return function($arg) use ($fs) { return array_reduce(array_reverse($fs), function($c, $f) { return $f($c); }, $arg); }; } function map($callable) { return function($list) use ($callable) { return array_map($callable, $list); }; } function aaray_column($column) { return function($array) use ($column) { return array_column($array, $column); }; } function aaray_slice($start) { return function($length) use ($start) { return function($array) use ($length, $start) { return array_slice($array, $start, $length); }; }; } function filter($callable) { return function($list) use ($callable) { return array_filter($list, $callable); }; } function f∘(callable $f) { return function(callable $g) use ($f) { return function($arg) use($g, $f) { return $f($g($arg)); }; }; } function ∘f(callable $f) { return function(callable $g) use ($f) { return function($arg) use($g, $f) { return $g($f($arg)); }; }; } function ∪($a, $b) { return array_merge($a, $b); } function getSeason(int $monthNum) { return ['summer', 'autumn', 'winter', 'spring'][floor(($monthNum%12)/3)]; } function getMonthName($monthNum) { return ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][$monthNum-1]; } // XXX: Consider renaming these to "is[First/Last]WeekOf[Month/Season]" function isStartOfSeason($monthNum, $dayNum) { return ($monthNum)%3 == 0 && isStartOfMonth($dayNum); } function isStartOfMonth($dayNum) { return $dayNum < 8; } function isEndOfSeason($yearNum, $monthNum, $dayNum) { return ($monthNum+1)%3 == 0 && isEndOfMonth($yearNum, $monthNum, $dayNum); } function isEndOfMonth($yearNum, $monthNum, $dayNum) { return $dayNum + 7 > cal_days_in_month(CAL_GREGORIAN, $monthNum, $yearNum); } function isEndOfYear($yearNum, $monthNum, $dayNum) { return $monthNum == 12 && isEndOfMonth($yearNum, $monthNum, $dayNum); } function getTasksForTheSeason($season, $taskMatrix) { return array_unique( array_reduce( $taskMatrix['annualy'][$season], function($c, $v) { return array_merge( $c, array_reduce($v, function($c, $v) { return array_merge($c, is_array($v) ? $v : [$v]); }, []) ); }, [] ) ); } function getTasksForTheMonth($monthNum, $taskMatrix) { return array_merge( $taskMatrix['monthly'], $monthNum % 6 == 0 ? $taskMatrix['biannualy'] : [], $monthNum % 3 == 0 ? $taskMatrix['quadriannualy'] : [], array_filter( $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)], function($v) { return !is_array($v); } ) ); } function getTasksForTheWeek(int $weekNum, int $monthNum, array $taskMatrix) { return array_merge( $weekNum % 2 == 0 ? $taskMatrix['bimonthly'] : [], $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)]['weekly'] ?? [], partition(4, getTasksForTheMonth($monthNum, $taskMatrix))[$weekNum-1] ); } const getFilePathForWeek = 'getFilePathForWeek'; function getFilePathForWeek(int $year, int $monthNum, int $weekNum) { // December is part of next year's summer $seasonYear = $year; return sprintf( 'tasks/%s/%s/%s/week%s.txt', $seasonYear, getSeason($monthNum), getMonthName($monthNum), $weekNum ); } function getFilePathsForMonth(int $year, int $monthNum) { return map(function($week) use ($year, $monthNum){ return getFilePathForWeek($year, $monthNum, $week); })(range(1,4)); } function getFilePathsForSeason(int $year, string $season) { return array_merge(...map(function($monthNum) use ($year) { // Summer of the current year includes december of the previous year. $seasonYear = $year - ($monthNum == 12 ? 1 : 0); return getFilePathsForMonth($seasonYear, $monthNum); })(array_filter(range(1,12), function($month) use ($season) { return getSeason($month) == $season; }))); } function getFilePathsForYear(int $year) { return array_merge(...map(function($season) use ($year) { return getFilePathsForSeason($year, $season); })(['summer', 'winter', 'spring', 'autumn'])); } function closest($n, $list) { $a = array_filter($list, function($value) use ($n) { return $value <= $n; }); arsort($a); return array_values($a)[0]; } function reveal($str) { $lastBytes = array_filter( (unpack('C*', $str)), function($key) { return $key % 4 == 0; }, ARRAY_FILTER_USE_KEY ); $penultimateBytes = array_filter( (unpack('C*', $str)), function($key) { return ($key + 1) % 4 == 0; }, ARRAY_FILTER_USE_KEY ); return implode('', zipWith(function($byte, $prevByte) { return chr($byte - ($prevByte == 133 ? 64 : 128)); }, $lastBytes, $penultimateBytes)); } function hide($str) { $charBytes = unpack('C*', $str); return pack(...array_merge(['C*'], array_merge(...map(function($charByte) { $magic = [243, 160, $charByte < 65 ? 132 : 133]; return array_merge($magic, [$charByte + ($charByte < 65 ? 128 : 64)]); })($charBytes)))); } function getMessagesFromInbox($inbox, array $rules, $unseenOnly = true) { return array_filter( array_map( function($rule, $service) use ($inbox, $unseenOnly) { $emails = imap_search($inbox, ['SEEN ', 'UNSEEN '][$unseenOnly] . $rule['imapQuery'], SE_UID); if(!$emails) { return []; } $messageTransform = $rule['messageTransform'] ?? 'identity'; $dateTransform = $rule['dateTransform'] ?? 'identity'; $body = quoted_printable_decode(imap_fetchbody($inbox, $emails[0], '1', FT_UID)); preg_match($rule['regex'], $messageTransform($body), $matches); return [ 'service' => $service, 'id' => substr(md5($body), 0, 6), 'uid' => $emails[0], 'due' => new DateTimeImmutable($dateTransform($matches['due'])), 'amount' => $matches['amount'] ]; }, $rules, array_keys($rules) ), function($e) { return !!$e; } ); }