a7c9d68d8fb57f1553843aa6831fbedbe5666063
1 <?php
declare(strict_types
=1);
3 require_once(__DIR__
. '/config.php');
4 require_once(AUTOLOAD_PATH
);
8 function getTelegram(): \Telegram\Bot\Api
{
10 return $tg = $tg ??
new \Telegram\Bot\
Api(BOT_TOKEN
);
13 function splitBill($amount) {
14 return floor($amount/2);
17 function identity($x) {
21 const notEmpty
= 'notEmpty';
22 function notEmpty($value) {
23 return !empty($value);
26 function getMessageSender($update) {
27 return PARTICIPANT_IDS
[getMessageSenderId($update)];
30 function getMessageSenderId($update) {
31 return $update->get('message')->get('from')->get('id');
34 function getMessageSenderDisplayName($update) {
35 return $update->get('message')->get('from')->get('first_name');
38 function canChatWith($update) {
39 return in_array($update->get('message')->get('from')->get('id'), array_keys(PARTICIPANT_IDS
));
42 function debug($whatever) {
48 function partition(int $numPartitions, $array) {
49 $partitionSize = (int)ceil(count($array) / $numPartitions);
52 array_values(filter(notEmpty
)(
53 map(function($p) use ($array, $partitionSize) {
54 return array_slice($array, $p*$partitionSize, $partitionSize);
55 })(range(0, $numPartitions-1))
59 function getInbox($inbox) {
62 if (!isset($inboxes[$inbox])) {
63 $inboxes[$inbox] = imap_open(
64 '{imap.gmail.com:993/debug/imap/ssl/novalidate-cert}' . $inbox,
70 return $inboxes[$inbox];
75 return $rules = $rules ??
require 'rules.php';
78 const getString
= 'getString';
79 function getString($identifier, ...$vars) {
81 $strings = $strings ??
require 'strings.php';
83 return isset($strings[$identifier]) ?
sprintf($strings[$identifier], ...$vars) : "[[$identifier]]";
86 const getStringAndCode
= 'getStringAndCode';
87 function getStringAndCode($string) {
88 return getString($string) . " (" . $string . ")";
91 function formatDate($date) {
92 return $date->format(DATE_FORMAT
);
95 function ssort($comparitor) {
96 return function($array) use ($comparitor) {
97 uasort($array, uncurry($comparitor));
102 function uncurry($f) {
103 return function($a, $b) use ($f) {
108 const sendToGroupChat
= 'sendToGroupChat';
109 function sendToGroupChat(string $message) {
110 return getTelegram()->sendMessage(['chat_id' => CHAT_ID
, 'text' => $message]);
113 const generateReminderText
= 'generateReminderText';
114 function generateReminderText($message) {
115 return getString('billreminder', REMIND_THRESHOLD
, $message['service'], splitBill($message['amount']), formatDate($message['due']));
118 const generateNewBillText
= 'generateNewBillText';
119 function generateNewBillText($message) {
120 return getString('newbill', $message['service'], splitBill($message['amount']), formatDate($message['due']));
123 const messageNeedsReminder
= 'messageNeedsReminder';
124 function messageNeedsReminder($message) {
125 return $message['due']->diff(new DateTimeImmutable
)->d
== REMIND_THRESHOLD
;
128 const lines
= 'lines';
129 function lines(string $string): array {
130 return explode("\n", $string);
134 function glue(string $delim): callable
{
135 return function(array $strings) use ($delim): string {
136 return implode($delim, $strings);
140 const unlines
= 'unlines';
141 function unlines($lines) {
142 return implode("\n", $lines);
145 const ununlines
= 'ununlines';
146 function ununlines($lines) {
147 return implode("\n\n", $lines);
150 const zipWith
= 'zipWith';
151 function zipWith(callable
$zipper, array $a, array $b) {
152 return array_map($zipper, $a, $b);
155 function field($field) {
156 return function($array) use ($field) {
157 return $array[$field];
163 return function($b) use ($a) {
170 return function($arg) use ($fs) {
171 return array_reduce(array_reverse($fs), function($c, $f) {
177 function map($callable) {
178 return function($list) use ($callable) {
179 return array_map($callable, $list);
183 function aaray_column($column) {
184 return function($array) use ($column) {
185 return array_column($array, $column);
189 function aaray_slice($start) {
190 return function($length) use ($start) {
191 return function($array) use ($length, $start) {
192 return array_slice($array, $start, $length);
197 function filter($callable) {
198 return function($list) use ($callable) {
199 return array_filter($list, $callable);
203 function f∘
(callable
$f) {
204 return function(callable
$g) use ($f) {
205 return function($arg) use($g, $f) {
211 function ∘
f(callable
$f) {
212 return function(callable
$g) use ($f) {
213 return function($arg) use($g, $f) {
220 return array_merge($a, $b);
223 function getSeason(int $monthNum) {
224 return ['summer', 'autumn', 'winter', 'spring'][floor(($monthNum%12
)/3)];
227 function getMonthName($monthNum) {
228 return ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][$monthNum-1];
231 // XXX: Consider renaming these to "is[First/Last]WeekOf[Month/Season]"
232 function isStartOfSeason($monthNum, $dayNum) {
233 return ($monthNum)%3
== 0 && isStartOfMonth($dayNum);
236 function isStartOfMonth($dayNum) {
240 function isEndOfSeason($yearNum, $monthNum, $dayNum) {
241 return ($monthNum+
1)%3
== 0 && isEndOfMonth($yearNum, $monthNum, $dayNum);
244 function isEndOfMonth($yearNum, $monthNum, $dayNum) {
245 return $dayNum +
7 > cal_days_in_month(CAL_GREGORIAN
, $monthNum, $yearNum);
248 function isEndOfYear($yearNum, $monthNum, $dayNum) {
249 return $monthNum == 12 && isEndOfMonth($yearNum, $monthNum, $dayNum);
252 function getTasksForTheSeason($season, $taskMatrix) {
255 $taskMatrix['annualy'][$season],
259 array_reduce($v, function($c, $v) {
260 return array_merge($c, is_array($v) ?
$v : [$v]);
269 function getTasksForTheMonth($monthNum, $taskMatrix) {
271 $taskMatrix['monthly'],
272 $monthNum %
6 == 0 ?
$taskMatrix['biannualy'] : [],
273 $monthNum %
3 == 0 ?
$taskMatrix['quadriannualy'] : [],
275 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)],
277 return !is_array($v);
283 function getTasksForTheWeek(int $weekNum, int $monthNum, array $taskMatrix) {
285 $weekNum %
2 == 0 ?
$taskMatrix['bimonthly'] : [],
286 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)]['weekly'] ??
[],
287 partition(4, getTasksForTheMonth($monthNum, $taskMatrix))[$weekNum-1]
291 const getFilePathForWeek
= 'getFilePathForWeek';
292 function getFilePathForWeek(int $year, int $monthNum, int $weekNum) {
293 // December is part of next year's summer
296 'tasks/%s/%s/%s/week%s.txt',
298 getSeason($monthNum),
299 getMonthName($monthNum),
304 function getFilePathsForMonth(int $year, int $monthNum) {
305 return map(function($week) use ($year, $monthNum){
306 return getFilePathForWeek($year, $monthNum, $week);
310 function getFilePathsForSeason(int $year, string $season) {
311 return array_merge(...map(function($monthNum) use ($year) {
312 // Summer of the current year includes december of the previous year.
313 $seasonYear = $year - ($monthNum == 12 ?
1 : 0);
314 return getFilePathsForMonth($seasonYear, $monthNum);
315 })(array_filter(range(1,12), function($month) use ($season) {
316 return getSeason($month) == $season;
320 function getFilePathsForYear(int $year) {
321 return array_merge(...map(function($season) use ($year) {
322 return getFilePathsForSeason($year, $season);
323 })(['summer', 'winter', 'spring', 'autumn']));
326 function closest($n, $list) {
327 $a = array_filter($list, function($value) use ($n) {
332 return array_values($a)[0];
335 function reveal($str) {
336 $lastBytes = array_filter(
337 (unpack('C*', $str)),
339 return $key %
4 == 0;
344 $penultimateBytes = array_filter(
345 (unpack('C*', $str)),
347 return ($key +
1) %
4 == 0;
352 return implode('', zipWith(function($byte, $prevByte) {
353 return chr($byte - ($prevByte == 133 ?
64 : 128));
354 }, $lastBytes, $penultimateBytes));
357 function hide($str) {
358 $charBytes = unpack('C*', $str);
359 return pack(...array_merge(['C*'], array_merge(...map(function($charByte) {
360 $magic = [243, 160, $charByte < 65 ?
132 : 133];
361 return array_merge($magic, [$charByte +
($charByte < 65 ?
128 : 64)]);
365 function getMessagesFromInbox($inbox, array $rules, $unseenOnly = true
) {
368 function($rule, $service) use ($inbox, $unseenOnly) {
369 $emails = imap_search($inbox, ['SEEN ', 'UNSEEN '][$unseenOnly] . $rule['imapQuery'], SE_UID
);
375 $messageTransform = $rule['messageTransform'] ??
'identity';
376 $dateTransform = $rule['dateTransform'] ??
'identity';
378 $body = quoted_printable_decode(imap_fetchbody($inbox, $emails[0], '1', FT_UID
));
379 preg_match($rule['regex'], $messageTransform($body), $matches);
382 'service' => $service,
383 'id' => substr(md5($body), 0, 6),
385 'due' => new DateTimeImmutable($dateTransform($matches['due'])),
386 'amount' => $matches['amount']