Make sure to add 1 when using closestIndex to find the current week of the month
[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 function getTelegram(): TelegramAPI {
10 STATIC $tg;
11 return $tg = $tg ?? new TelegramAPI(BOT_TOKEN);
12 }
13
14 function splitBill(float $amount) : float {
15 return floor($amount/2);
16 }
17
18 function identity($x) {
19 return $x;
20 }
21
22 const notEmpty = 'notEmpty';
23 function notEmpty($value) : bool {
24 return !empty($value);
25 }
26
27 function getMessageSender(TelegramUpdate $update) : string {
28 return PARTICIPANT_IDS[getMessageSenderId($update)];
29 }
30
31 function getMessageSenderId(TelegramUpdate $update) : int {
32 return $update->get('message')->get('from')->get('id');
33 }
34
35 function getMessageSenderDisplayName(TelegramUpdate $update) : string {
36 return $update->get('message')->get('from')->get('first_name');
37 }
38
39 function canChatWith(TelegramUpdate $update) : bool {
40 return in_array($update->get('message')->get('from')->get('id'), array_keys(PARTICIPANT_IDS));
41 }
42
43 function partition(int $numPartitions, array $array) : array {
44 $partitionSize = (int)ceil(count($array) / $numPartitions);
45
46 return array_values(filter(notEmpty)(
47 map(function($p) use ($array, $partitionSize) {
48 return array_slice($array, $p*$partitionSize, $partitionSize);
49 })(range(0, $numPartitions-1))
50 ));
51 }
52
53 function getInbox(string $inbox) {
54 STATIC $inboxes;
55
56 if (!isset($inboxes[$inbox])) {
57 $inboxes[$inbox] = imap_open(
58 '{imap.gmail.com:993/debug/imap/ssl/novalidate-cert}' . $inbox,
59 EMAIL,
60 PASSWORD
61 );
62 }
63
64 return $inboxes[$inbox];
65 }
66
67 function getRules() {
68 STATIC $rules;
69 return $rules = $rules ?? require 'rules.php';
70 }
71
72 const getString = 'getString';
73 function getString($identifier, ...$vars) {
74 STATIC $strings;
75 $strings = $strings ?? require 'strings.php';
76
77 return isset($strings[$identifier]) ? sprintf($strings[$identifier], ...$vars) : "[[$identifier]]";
78 }
79
80 const getStringAndCode = 'getStringAndCode';
81 function getStringAndCode($string) {
82 return getString($string) . " (" . $string . ")";
83 };
84
85 function formatDate($date) {
86 return $date->format(DATE_FORMAT);
87 }
88
89 function ssort($comparitor) {
90 return function($array) use ($comparitor) {
91 uasort($array, uncurry($comparitor));
92 return $array;
93 };
94 }
95
96 function uncurry($f) {
97 return function($a, $b) use ($f) {
98 return $f($a)($b);
99 };
100 }
101
102 function between($content, $start){
103 $r = explode($start, $content);
104 if (isset($r[1])){
105 $r = explode($start, $r[1]);
106 return $r[0];
107 }
108 return '';
109 }
110
111 const sendToGroupChat = 'sendToGroupChat';
112 function sendToGroupChat(string $message) {
113 return getTelegram()->sendMessage(['chat_id' => CHAT_ID, 'text' => $message]);
114 }
115
116 const generateReminderText = 'generateReminderText';
117 function generateReminderText($message) {
118 return getString('billreminder', REMIND_THRESHOLD, $message['service'], splitBill($message['amount']), formatDate($message['due']));
119 }
120
121 const generateNewBillText = 'generateNewBillText';
122 function generateNewBillText($message) {
123 return getString('newbill', $message['service'], splitBill($message['amount']), formatDate($message['due']));
124 }
125
126 const messageNeedsReminder = 'messageNeedsReminder';
127 function messageNeedsReminder($message) {
128 return $message['due']->diff(new DateTimeImmutable)->d == REMIND_THRESHOLD;
129 }
130
131 const lines = 'lines';
132 function lines(string $string): array {
133 return explode("\n", $string);
134 }
135
136 const glue = 'glue';
137 function glue(string $delim): callable {
138 return function(array $strings) use ($delim): string {
139 return implode($delim, $strings);
140 };
141 }
142
143 const unlines = 'unlines';
144 function unlines($lines) {
145 return implode("\n", $lines);
146 }
147
148 const ununlines = 'ununlines';
149 function ununlines($lines) {
150 return implode("\n\n", $lines);
151 }
152
153 const zipWith = 'zipWith';
154 function zipWith(callable $zipper, array $a, array $b) {
155 return array_map($zipper, $a, $b);
156 }
157
158 function field($field) {
159 return function($array) use ($field) {
160 return $array[$field];
161 };
162 }
163
164 const= '⬄';
165 function($a) {
166 return function($b) use ($a) {
167 return $a <=> $b;
168 };
169 }
170
171
172 function(...$fs) {
173 return function($arg) use ($fs) {
174 return array_reduce(array_reverse($fs), function($c, $f) {
175 return $f($c);
176 }, $arg);
177 };
178 }
179
180 function map($callable) {
181 return function($list) use ($callable) {
182 return array_map($callable, $list);
183 };
184 }
185
186 function aaray_column($column) {
187 return function($array) use ($column) {
188 return array_column($array, $column);
189 };
190 }
191
192 function aaray_slice($start) {
193 return function($length) use ($start) {
194 return function($array) use ($length, $start) {
195 return array_slice($array, $start, $length);
196 };
197 };
198 }
199
200 function filter($callable) {
201 return function($list) use ($callable) {
202 return array_filter($list, $callable);
203 };
204 }
205
206 function f∘(callable $f) {
207 return function(callable $g) use ($f) {
208 return function($arg) use($g, $f) {
209 return $f($g($arg));
210 };
211 };
212 }
213
214 functionf(callable $f) {
215 return function(callable $g) use ($f) {
216 return function($arg) use($g, $f) {
217 return $g($f($arg));
218 };
219 };
220 }
221
222 function($a, $b) {
223 return array_merge($a, $b);
224 }
225
226 function getSeason(int $monthNum) {
227 return ['summer', 'autumn', 'winter', 'spring'][(int)floor(($monthNum%12)/3)];
228 }
229
230 function getMonthName($monthNum) {
231 return ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'][$monthNum-1];
232 }
233
234 // XXX: Consider renaming these to "is[First/Last]WeekOf[Month/Season]"
235 function isStartOfSeason($monthNum, $dayNum) {
236 return ($monthNum)%3 == 0 && isStartOfMonth($dayNum);
237 }
238
239 function isStartOfMonth($dayNum) {
240 return $dayNum < 8;
241 }
242
243 function isEndOfSeason($yearNum, $monthNum, $dayNum) {
244 return ($monthNum+1)%3 == 0 && isEndOfMonth($yearNum, $monthNum, $dayNum);
245 }
246
247 function isEndOfMonth($yearNum, $monthNum, $dayNum) {
248 return $dayNum + 7 > cal_days_in_month(CAL_GREGORIAN, $monthNum, $yearNum);
249 }
250
251 function isEndOfYear($yearNum, $monthNum, $dayNum) {
252 return $monthNum == 12 && isEndOfMonth($yearNum, $monthNum, $dayNum);
253 }
254
255 function getTasksForTheSeason($season, $taskMatrix) {
256 return array_unique(
257 array_reduce(
258 $taskMatrix['annualy'][$season],
259 function($c, $v) {
260 return array_merge(
261 $c,
262 array_reduce($v, function($c, $v) {
263 return array_merge($c, is_array($v) ? $v : [$v]);
264 }, [])
265 );
266 },
267 []
268 )
269 );
270 }
271
272 function getTasksForTheMonth($monthNum, $taskMatrix) {
273 return array_merge(
274 $taskMatrix['monthly'],
275 $monthNum % 6 == 0 ? $taskMatrix['biannualy'] : [],
276 $monthNum % 3 == 0 ? $taskMatrix['quadriannualy'] : [],
277 array_filter(
278 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)],
279 function($v) {
280 return !is_array($v);
281 }
282 )
283 );
284 }
285
286 function getTasksForTheWeek(int $weekNum, int $monthNum, array $taskMatrix) {
287 return array_merge(
288 $weekNum % 2 == 0 ? $taskMatrix['bimonthly'] : [],
289 $taskMatrix['annualy'][getSeason($monthNum)][getMonthName($monthNum)]['weekly'] ?? [],
290 partition(4, getTasksForTheMonth($monthNum, $taskMatrix))[$weekNum-1]
291 );
292 }
293
294 const getFilePathForWeek = 'getFilePathForWeek';
295 function getFilePathForWeek(int $year, int $monthNum, int $weekNum) {
296 // December is part of next year's summer
297 $seasonYear = $year;
298 return sprintf(
299 'tasks/%s/%s/%s/week%s.txt',
300 $seasonYear,
301 getSeason($monthNum),
302 getMonthName($monthNum),
303 $weekNum
304 );
305 }
306
307 function getFilePathsForMonth(int $year, int $monthNum) {
308 return map(function($week) use ($year, $monthNum){
309 return getFilePathForWeek($year, $monthNum, $week);
310 })(range(1,4));
311 }
312
313 function getFilePathsForSeason(int $year, string $season) {
314 return array_merge(...map(function($monthNum) use ($year) {
315 // Summer of the current year includes december of the previous year.
316 $seasonYear = $year - ($monthNum == 12 ? 1 : 0);
317 return getFilePathsForMonth($seasonYear, $monthNum);
318 })(array_filter(range(1,12), function($month) use ($season) {
319 return getSeason($month) == $season;
320 })));
321 }
322
323 function getFilePathsForYear(int $year) {
324 return array_merge(...map(function($season) use ($year) {
325 return getFilePathsForSeason($year, $season);
326 })(['summer', 'winter', 'spring', 'autumn']));
327 }
328
329 function closestIndex($n, array $list) {
330 $a = map(function(int $v) use ($n) : int {
331 return (int)abs($v - $n);
332 })
333 ($list);
334
335 asort($a);
336 return array_keys($a)[0];
337 }
338
339 function closest($n, array $list) : int {
340 return $list[closestIndex($n, $list)];
341 }
342
343 function reveal($str) {
344 $lastBytes = array_filter(
345 (unpack('C*', $str)),
346 function($key) {
347 return $key % 4 == 0;
348 },
349 ARRAY_FILTER_USE_KEY
350 );
351
352 $penultimateBytes = array_filter(
353 (unpack('C*', $str)),
354 function($key) {
355 return ($key + 1) % 4 ==
356 0;
357 },
358 ARRAY_FILTER_USE_KEY
359 );
360
361 return implode('', zipWith(function($byte, $prevByte) {
362 return chr($byte - ($prevByte == 133 ? 64 : 128));
363 }, $lastBytes, $penultimateBytes));
364 }
365
366 function hide($str) {
367 $charBytes = unpack('C*', $str);
368 return pack(...array_merge(['C*'], array_merge(...map(function($charByte) {
369 $magic = [243, 160, $charByte < 65 ? 132 : 133];
370 return array_merge($magic, [$charByte + ($charByte < 65 ? 128 : 64)]);
371 })($charBytes))));
372 }
373
374 function getMessagesFromInbox($inbox, array $rules, $unseenOnly = true) {
375 return array_filter(
376 array_map(
377 function($rule, $service) use ($inbox, $unseenOnly) {
378 $emails = imap_search($inbox, ['SEEN ', 'UNSEEN '][$unseenOnly] . $rule['imapQuery'], SE_UID);
379
380 if(!$emails) {
381 return [];
382 }
383
384 $messageTransform = $rule['messageTransform'] ?? 'identity';
385 $dateTransform = $rule['dateTransform'] ?? 'identity';
386
387 $body = quoted_printable_decode(imap_fetchbody($inbox, $emails[0], '1', FT_UID));
388 preg_match($rule['regex'], $messageTransform($body), $matches);
389
390 return [
391 'service' => $service,
392 'id' => substr(md5($body), 0, 6),
393 'uid' => $emails[0],
394 'due' => new DateTimeImmutable($dateTransform($matches['due'])),
395 'amount' => $matches['amount']
396 ];
397 },
398 $rules,
399 array_keys($rules)
400 ),
401 function($e) {
402 return !!$e;
403 }
404 );
405 }