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