From 115e78dc583f6762b4f93e0794223059b9df42b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sn=C3=ADda=C5=88ov=C3=BD=20Mistr?= Date: Sat, 16 Mar 2024 12:16:46 +0100 Subject: [PATCH] =?UTF-8?q?=C5=BDij=C3=AD=20v=20m=C3=BDch=20zdech?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + .vscode/settings.json | 3 + Cargo.lock | 3411 +++++++++++++++++ Cargo.toml | 54 + Nekrochan.toml.template | 47 + README.md | 5 + askama.toml | 2 + build.rs | 39 + configure.sh | 68 + migrations/20230710121446_create_tables.sql | 28 + migrations/20231216092451_global_banners.sql | 6 + migrations/20231217111814_create_news.sql | 8 + .../20231229180942_remove_references.sql | 2 + src/auth.rs | 39 + src/cfg.rs | 79 + src/ctx.rs | 48 + src/db/account.rs | 117 + src/db/ban.rs | 107 + src/db/banner.rs | 64 + src/db/board.rs | 249 ++ src/db/cache.rs | 100 + src/db/local_stats.rs | 30 + src/db/mod.rs | 10 + src/db/models.rs | 107 + src/db/newspost.rs | 74 + src/db/post.rs | 594 +++ src/error.rs | 269 ++ src/files.rs | 299 ++ src/filters.rs | 87 + src/lib.rs | 61 + src/live_hub.rs | 272 ++ src/live_session.rs | 73 + src/main.rs | 188 + src/markup.rs | 229 ++ src/perms.rs | 111 + src/qsform.rs | 43 + src/schedule.rs | 67 + src/trip.rs | 54 + src/web/actions/appeal_ban.rs | 59 + src/web/actions/create_post.rs | 341 ++ src/web/actions/edit_posts.rs | 76 + src/web/actions/mod.rs | 46 + src/web/actions/report_posts.rs | 110 + src/web/actions/staff_post_actions.rs | 369 ++ src/web/actions/user_post_actions.rs | 135 + src/web/board.rs | 74 + src/web/board_catalog.rs | 46 + src/web/captcha.rs | 55 + src/web/edit_posts.rs | 42 + src/web/index.rs | 43 + src/web/ip_posts.rs | 65 + src/web/live.rs | 62 + src/web/login.rs | 59 + src/web/logout.rs | 13 + src/web/mod.rs | 53 + src/web/news.rs | 21 + src/web/overboard.rs | 68 + src/web/overboard_catalog.rs | 30 + src/web/page.rs | 36 + src/web/search.rs | 88 + src/web/staff/account.rs | 28 + src/web/staff/accounts.rs | 31 + src/web/staff/actions/add_banners.rs | 42 + src/web/staff/actions/change_password.rs | 38 + src/web/staff/actions/create_account.rs | 49 + src/web/staff/actions/create_board.rs | 56 + src/web/staff/actions/create_news.rs | 48 + src/web/staff/actions/delete_account.rs | 23 + src/web/staff/actions/edit_news.rs | 71 + src/web/staff/actions/mod.rs | 16 + src/web/staff/actions/remove_accounts.rs | 42 + src/web/staff/actions/remove_banners.rs | 38 + src/web/staff/actions/remove_bans.rs | 37 + src/web/staff/actions/remove_boards.rs | 37 + src/web/staff/actions/remove_news.rs | 65 + src/web/staff/actions/transfer_ownership.rs | 39 + src/web/staff/actions/update_board_config.rs | 105 + src/web/staff/actions/update_boards.rs | 57 + src/web/staff/actions/update_permissions.rs | 128 + src/web/staff/banners.rs | 30 + src/web/staff/bans.rs | 31 + src/web/staff/board_config.rs | 42 + src/web/staff/boards.rs | 31 + src/web/staff/edit_news.rs | 49 + src/web/staff/mod.rs | 11 + src/web/staff/news.rs | 31 + src/web/staff/permissions.rs | 42 + src/web/staff/reports.rs | 63 + src/web/tcx.rs | 116 + src/web/thread.rs | 59 + src/web/thread_json.rs | 58 + static/default-banner.png | Bin 0 -> 19816 bytes static/favicon.ico | Bin 0 -> 4286 bytes static/flags/ad.png | Bin 0 -> 643 bytes static/flags/ae.png | Bin 0 -> 408 bytes static/flags/af.png | Bin 0 -> 604 bytes static/flags/ag.png | Bin 0 -> 591 bytes static/flags/ai.png | Bin 0 -> 643 bytes static/flags/al.png | Bin 0 -> 600 bytes static/flags/am.png | Bin 0 -> 497 bytes static/flags/an.png | Bin 0 -> 488 bytes static/flags/ao.png | Bin 0 -> 428 bytes static/flags/ar.png | Bin 0 -> 506 bytes static/flags/as.png | Bin 0 -> 647 bytes static/flags/at.png | Bin 0 -> 403 bytes static/flags/au.png | Bin 0 -> 673 bytes static/flags/aw.png | Bin 0 -> 524 bytes static/flags/ax.png | Bin 0 -> 663 bytes static/flags/az.png | Bin 0 -> 589 bytes static/flags/ba.png | Bin 0 -> 593 bytes static/flags/bb.png | Bin 0 -> 585 bytes static/flags/bd.png | Bin 0 -> 504 bytes static/flags/be.png | Bin 0 -> 449 bytes static/flags/bf.png | Bin 0 -> 497 bytes static/flags/bg.png | Bin 0 -> 462 bytes static/flags/bh.png | Bin 0 -> 457 bytes static/flags/bi.png | Bin 0 -> 675 bytes static/flags/bj.png | Bin 0 -> 486 bytes static/flags/bm.png | Bin 0 -> 611 bytes static/flags/bn.png | Bin 0 -> 639 bytes static/flags/bo.png | Bin 0 -> 500 bytes static/flags/br.png | Bin 0 -> 593 bytes static/flags/bs.png | Bin 0 -> 526 bytes static/flags/bt.png | Bin 0 -> 631 bytes static/flags/bv.png | Bin 0 -> 512 bytes static/flags/bw.png | Bin 0 -> 443 bytes static/flags/by.png | Bin 0 -> 514 bytes static/flags/bz.png | Bin 0 -> 600 bytes static/flags/ca.png | Bin 0 -> 628 bytes static/flags/cc.png | Bin 0 -> 625 bytes static/flags/cd.png | Bin 0 -> 528 bytes static/flags/cf.png | Bin 0 -> 614 bytes static/flags/cg.png | Bin 0 -> 521 bytes static/flags/ch.png | Bin 0 -> 367 bytes static/flags/ci.png | Bin 0 -> 453 bytes static/flags/ck.png | Bin 0 -> 586 bytes static/flags/cl.png | Bin 0 -> 450 bytes static/flags/cm.png | Bin 0 -> 525 bytes static/flags/cn.png | Bin 0 -> 472 bytes static/flags/co.png | Bin 0 -> 483 bytes static/flags/cr.png | Bin 0 -> 477 bytes static/flags/cs.png | Bin 0 -> 439 bytes static/flags/cu.png | Bin 0 -> 563 bytes static/flags/cv.png | Bin 0 -> 529 bytes static/flags/cx.png | Bin 0 -> 608 bytes static/flags/cy.png | Bin 0 -> 428 bytes static/flags/cz.png | Bin 0 -> 476 bytes static/flags/de.png | Bin 0 -> 545 bytes static/flags/dj.png | Bin 0 -> 572 bytes static/flags/dk.png | Bin 0 -> 495 bytes static/flags/dm.png | Bin 0 -> 620 bytes static/flags/do.png | Bin 0 -> 508 bytes static/flags/dz.png | Bin 0 -> 582 bytes static/flags/ec.png | Bin 0 -> 500 bytes static/flags/ee.png | Bin 0 -> 429 bytes static/flags/eg.png | Bin 0 -> 465 bytes static/flags/eh.png | Bin 0 -> 508 bytes static/flags/er.png | Bin 0 -> 653 bytes static/flags/es.png | Bin 0 -> 469 bytes static/flags/et.png | Bin 0 -> 592 bytes static/flags/fam.png | Bin 0 -> 532 bytes static/flags/fi.png | Bin 0 -> 489 bytes static/flags/fj.png | Bin 0 -> 610 bytes static/flags/fk.png | Bin 0 -> 648 bytes static/flags/fm.png | Bin 0 -> 552 bytes static/flags/fo.png | Bin 0 -> 474 bytes static/flags/fr.png | Bin 0 -> 545 bytes static/flags/ga.png | Bin 0 -> 489 bytes static/flags/gb.png | Bin 0 -> 599 bytes static/flags/gd.png | Bin 0 -> 637 bytes static/flags/ge.png | Bin 0 -> 594 bytes static/flags/gf.png | Bin 0 -> 545 bytes static/flags/gh.png | Bin 0 -> 490 bytes static/flags/gi.png | Bin 0 -> 463 bytes static/flags/gl.png | Bin 0 -> 470 bytes static/flags/gm.png | Bin 0 -> 493 bytes static/flags/gn.png | Bin 0 -> 480 bytes static/flags/gp.png | Bin 0 -> 488 bytes static/flags/gq.png | Bin 0 -> 537 bytes static/flags/gr.png | Bin 0 -> 487 bytes static/flags/gs.png | Bin 0 -> 630 bytes static/flags/gt.png | Bin 0 -> 493 bytes static/flags/gu.png | Bin 0 -> 509 bytes static/flags/gw.png | Bin 0 -> 516 bytes static/flags/gy.png | Bin 0 -> 645 bytes static/flags/hk.png | Bin 0 -> 527 bytes static/flags/hm.png | Bin 0 -> 673 bytes static/flags/hn.png | Bin 0 -> 537 bytes static/flags/hr.png | Bin 0 -> 524 bytes static/flags/ht.png | Bin 0 -> 487 bytes static/flags/hu.png | Bin 0 -> 432 bytes static/flags/id.png | Bin 0 -> 430 bytes static/flags/ie.png | Bin 0 -> 481 bytes static/flags/il.png | Bin 0 -> 431 bytes static/flags/in.png | Bin 0 -> 503 bytes static/flags/io.png | Bin 0 -> 658 bytes static/flags/iq.png | Bin 0 -> 515 bytes static/flags/ir.png | Bin 0 -> 512 bytes static/flags/is.png | Bin 0 -> 532 bytes static/flags/it.png | Bin 0 -> 420 bytes static/flags/jm.png | Bin 0 -> 637 bytes static/flags/jo.png | Bin 0 -> 473 bytes static/flags/jp.png | Bin 0 -> 420 bytes static/flags/ke.png | Bin 0 -> 569 bytes static/flags/kg.png | Bin 0 -> 510 bytes static/flags/kh.png | Bin 0 -> 549 bytes static/flags/ki.png | Bin 0 -> 656 bytes static/flags/km.png | Bin 0 -> 577 bytes static/flags/kn.png | Bin 0 -> 604 bytes static/flags/kp.png | Bin 0 -> 561 bytes static/flags/kr.png | Bin 0 -> 592 bytes static/flags/kw.png | Bin 0 -> 486 bytes static/flags/ky.png | Bin 0 -> 643 bytes static/flags/kz.png | Bin 0 -> 616 bytes static/flags/la.png | Bin 0 -> 563 bytes static/flags/lb.png | Bin 0 -> 517 bytes static/flags/lc.png | Bin 0 -> 520 bytes static/flags/li.png | Bin 0 -> 537 bytes static/flags/lk.png | Bin 0 -> 627 bytes static/flags/lr.png | Bin 0 -> 466 bytes static/flags/ls.png | Bin 0 -> 628 bytes static/flags/lt.png | Bin 0 -> 508 bytes static/flags/lu.png | Bin 0 -> 481 bytes static/flags/lv.png | Bin 0 -> 465 bytes static/flags/ly.png | Bin 0 -> 419 bytes static/flags/ma.png | Bin 0 -> 432 bytes static/flags/mc.png | Bin 0 -> 380 bytes static/flags/md.png | Bin 0 -> 566 bytes static/flags/me.png | Bin 0 -> 448 bytes static/flags/mg.png | Bin 0 -> 453 bytes static/flags/mh.png | Bin 0 -> 628 bytes static/flags/mk.png | Bin 0 -> 664 bytes static/flags/ml.png | Bin 0 -> 474 bytes static/flags/mm.png | Bin 0 -> 483 bytes static/flags/mn.png | Bin 0 -> 492 bytes static/flags/mo.png | Bin 0 -> 588 bytes static/flags/mp.png | Bin 0 -> 597 bytes static/flags/mq.png | Bin 0 -> 655 bytes static/flags/mr.png | Bin 0 -> 569 bytes static/flags/ms.png | Bin 0 -> 614 bytes static/flags/mt.png | Bin 0 -> 420 bytes static/flags/mu.png | Bin 0 -> 496 bytes static/flags/mv.png | Bin 0 -> 542 bytes static/flags/mw.png | Bin 0 -> 529 bytes static/flags/mx.png | Bin 0 -> 574 bytes static/flags/my.png | Bin 0 -> 571 bytes static/flags/mz.png | Bin 0 -> 584 bytes static/flags/na.png | Bin 0 -> 647 bytes static/flags/nc.png | Bin 0 -> 591 bytes static/flags/ne.png | Bin 0 -> 537 bytes static/flags/nf.png | Bin 0 -> 602 bytes static/flags/ng.png | Bin 0 -> 482 bytes static/flags/ni.png | Bin 0 -> 508 bytes static/flags/nl.png | Bin 0 -> 453 bytes static/flags/no.png | Bin 0 -> 512 bytes static/flags/np.png | Bin 0 -> 443 bytes static/flags/nr.png | Bin 0 -> 527 bytes static/flags/nu.png | Bin 0 -> 572 bytes static/flags/nz.png | Bin 0 -> 639 bytes static/flags/om.png | Bin 0 -> 478 bytes static/flags/pa.png | Bin 0 -> 519 bytes static/flags/pe.png | Bin 0 -> 397 bytes static/flags/pf.png | Bin 0 -> 498 bytes static/flags/pg.png | Bin 0 -> 593 bytes static/flags/ph.png | Bin 0 -> 538 bytes static/flags/pk.png | Bin 0 -> 569 bytes static/flags/pl.png | Bin 0 -> 374 bytes static/flags/pm.png | Bin 0 -> 689 bytes static/flags/pn.png | Bin 0 -> 657 bytes static/flags/pr.png | Bin 0 -> 556 bytes static/flags/ps.png | Bin 0 -> 472 bytes static/flags/pt.png | Bin 0 -> 554 bytes static/flags/pw.png | Bin 0 -> 550 bytes static/flags/py.png | Bin 0 -> 473 bytes static/flags/qa.png | Bin 0 -> 450 bytes static/flags/re.png | Bin 0 -> 545 bytes static/flags/ro.png | Bin 0 -> 495 bytes static/flags/rs.png | Bin 0 -> 423 bytes static/flags/ru.png | Bin 0 -> 420 bytes static/flags/rw.png | Bin 0 -> 533 bytes static/flags/sa.png | Bin 0 -> 551 bytes static/flags/sb.png | Bin 0 -> 624 bytes static/flags/sc.png | Bin 0 -> 608 bytes static/flags/sd.png | Bin 0 -> 492 bytes static/flags/se.png | Bin 0 -> 542 bytes static/flags/sg.png | Bin 0 -> 468 bytes static/flags/sh.png | Bin 0 -> 645 bytes static/flags/si.png | Bin 0 -> 510 bytes static/flags/sj.png | Bin 0 -> 512 bytes static/flags/sk.png | Bin 0 -> 562 bytes static/flags/sl.png | Bin 0 -> 436 bytes static/flags/sm.png | Bin 0 -> 502 bytes static/flags/sn.png | Bin 0 -> 532 bytes static/flags/so.png | Bin 0 -> 527 bytes static/flags/sr.png | Bin 0 -> 513 bytes static/flags/st.png | Bin 0 -> 584 bytes static/flags/sv.png | Bin 0 -> 501 bytes static/flags/sy.png | Bin 0 -> 422 bytes static/flags/sz.png | Bin 0 -> 643 bytes static/flags/tc.png | Bin 0 -> 624 bytes static/flags/td.png | Bin 0 -> 570 bytes static/flags/tf.png | Bin 0 -> 527 bytes static/flags/tg.png | Bin 0 -> 562 bytes static/flags/th.png | Bin 0 -> 452 bytes static/flags/tj.png | Bin 0 -> 496 bytes static/flags/tk.png | Bin 0 -> 638 bytes static/flags/tl.png | Bin 0 -> 514 bytes static/flags/tm.png | Bin 0 -> 593 bytes static/flags/tn.png | Bin 0 -> 495 bytes static/flags/to.png | Bin 0 -> 426 bytes static/flags/tr.png | Bin 0 -> 492 bytes static/flags/tt.png | Bin 0 -> 617 bytes static/flags/tv.png | Bin 0 -> 536 bytes static/flags/tw.png | Bin 0 -> 465 bytes static/flags/tz.png | Bin 0 -> 642 bytes static/flags/ua.png | Bin 0 -> 446 bytes static/flags/ug.png | Bin 0 -> 531 bytes static/flags/um.png | Bin 0 -> 571 bytes static/flags/us.png | Bin 0 -> 609 bytes static/flags/uy.png | Bin 0 -> 532 bytes static/flags/uz.png | Bin 0 -> 515 bytes static/flags/va.png | Bin 0 -> 553 bytes static/flags/vc.png | Bin 0 -> 577 bytes static/flags/ve.png | Bin 0 -> 528 bytes static/flags/vg.png | Bin 0 -> 630 bytes static/flags/vi.png | Bin 0 -> 616 bytes static/flags/vn.png | Bin 0 -> 474 bytes static/flags/vu.png | Bin 0 -> 604 bytes static/flags/wf.png | Bin 0 -> 554 bytes static/flags/ws.png | Bin 0 -> 476 bytes static/flags/xx.png | Bin 0 -> 447 bytes static/flags/ye.png | Bin 0 -> 413 bytes static/flags/yt.png | Bin 0 -> 593 bytes static/flags/za.png | Bin 0 -> 642 bytes static/flags/zm.png | Bin 0 -> 500 bytes static/flags/zw.png | Bin 0 -> 574 bytes static/icons/locked.png | Bin 0 -> 881 bytes static/icons/sticky.png | Bin 0 -> 754 bytes static/js/autofill.js | 52 + static/js/captcha.js | 25 + static/js/expand.js | 103 + static/js/hover.js | 138 + static/js/jquery.min.js | 2 + static/js/live.js | 73 + static/js/post-form.js | 173 + static/js/quote.js | 36 + static/js/time.js | 125 + static/spoiler.png | Bin 0 -> 1312 bytes static/style.css | 535 +++ static/themes/yotsuba-b.css | 39 + static/themes/yotsuba.css | 39 + templates/action.html | 21 + templates/banned.html | 67 + templates/base.html | 63 + templates/board-catalog.html | 42 + templates/board.html | 58 + templates/edit-posts.html | 20 + templates/error.html | 31 + templates/index.html | 65 + templates/ip-posts.html | 29 + templates/login.html | 24 + templates/macros/board-links.html | 8 + templates/macros/catalog-entry.html | 23 + templates/macros/pagination.html | 25 + templates/macros/post-actions.html | 224 ++ templates/macros/post-form.html | 105 + templates/macros/post.html | 77 + templates/macros/staff-nav.html | 26 + templates/macros/static-pagination.html | 22 + templates/news.html | 24 + templates/overboard-catalog.html | 40 + templates/overboard.html | 47 + templates/page.html | 3 + templates/search.html | 47 + templates/staff/account.html | 67 + templates/staff/accounts.html | 56 + templates/staff/banners.html | 45 + templates/staff/bans.html | 54 + templates/staff/board-config.html | 187 + templates/staff/boards.html | 75 + templates/staff/edit-news.html | 26 + templates/staff/news.html | 51 + templates/staff/permissions.html | 179 + templates/staff/reports.html | 40 + templates/thread.html | 55 + 385 files changed, 13720 insertions(+) create mode 100755 .gitignore create mode 100644 .vscode/settings.json create mode 100755 Cargo.lock create mode 100755 Cargo.toml create mode 100755 Nekrochan.toml.template create mode 100644 README.md create mode 100644 askama.toml create mode 100755 build.rs create mode 100755 configure.sh create mode 100755 migrations/20230710121446_create_tables.sql create mode 100644 migrations/20231216092451_global_banners.sql create mode 100644 migrations/20231217111814_create_news.sql create mode 100644 migrations/20231229180942_remove_references.sql create mode 100755 src/auth.rs create mode 100755 src/cfg.rs create mode 100755 src/ctx.rs create mode 100755 src/db/account.rs create mode 100755 src/db/ban.rs create mode 100644 src/db/banner.rs create mode 100755 src/db/board.rs create mode 100644 src/db/cache.rs create mode 100644 src/db/local_stats.rs create mode 100755 src/db/mod.rs create mode 100755 src/db/models.rs create mode 100644 src/db/newspost.rs create mode 100755 src/db/post.rs create mode 100755 src/error.rs create mode 100755 src/files.rs create mode 100644 src/filters.rs create mode 100755 src/lib.rs create mode 100644 src/live_hub.rs create mode 100644 src/live_session.rs create mode 100755 src/main.rs create mode 100644 src/markup.rs create mode 100755 src/perms.rs create mode 100644 src/qsform.rs create mode 100644 src/schedule.rs create mode 100755 src/trip.rs create mode 100644 src/web/actions/appeal_ban.rs create mode 100644 src/web/actions/create_post.rs create mode 100644 src/web/actions/edit_posts.rs create mode 100644 src/web/actions/mod.rs create mode 100644 src/web/actions/report_posts.rs create mode 100644 src/web/actions/staff_post_actions.rs create mode 100644 src/web/actions/user_post_actions.rs create mode 100755 src/web/board.rs create mode 100644 src/web/board_catalog.rs create mode 100644 src/web/captcha.rs create mode 100644 src/web/edit_posts.rs create mode 100755 src/web/index.rs create mode 100644 src/web/ip_posts.rs create mode 100644 src/web/live.rs create mode 100755 src/web/login.rs create mode 100755 src/web/logout.rs create mode 100755 src/web/mod.rs create mode 100644 src/web/news.rs create mode 100644 src/web/overboard.rs create mode 100644 src/web/overboard_catalog.rs create mode 100644 src/web/page.rs create mode 100644 src/web/search.rs create mode 100755 src/web/staff/account.rs create mode 100755 src/web/staff/accounts.rs create mode 100755 src/web/staff/actions/add_banners.rs create mode 100755 src/web/staff/actions/change_password.rs create mode 100755 src/web/staff/actions/create_account.rs create mode 100755 src/web/staff/actions/create_board.rs create mode 100644 src/web/staff/actions/create_news.rs create mode 100755 src/web/staff/actions/delete_account.rs create mode 100644 src/web/staff/actions/edit_news.rs create mode 100755 src/web/staff/actions/mod.rs create mode 100755 src/web/staff/actions/remove_accounts.rs create mode 100755 src/web/staff/actions/remove_banners.rs create mode 100755 src/web/staff/actions/remove_bans.rs create mode 100755 src/web/staff/actions/remove_boards.rs create mode 100644 src/web/staff/actions/remove_news.rs create mode 100755 src/web/staff/actions/transfer_ownership.rs create mode 100755 src/web/staff/actions/update_board_config.rs create mode 100755 src/web/staff/actions/update_boards.rs create mode 100755 src/web/staff/actions/update_permissions.rs create mode 100755 src/web/staff/banners.rs create mode 100755 src/web/staff/bans.rs create mode 100755 src/web/staff/board_config.rs create mode 100755 src/web/staff/boards.rs create mode 100644 src/web/staff/edit_news.rs create mode 100755 src/web/staff/mod.rs create mode 100644 src/web/staff/news.rs create mode 100755 src/web/staff/permissions.rs create mode 100755 src/web/staff/reports.rs create mode 100755 src/web/tcx.rs create mode 100644 src/web/thread.rs create mode 100644 src/web/thread_json.rs create mode 100644 static/default-banner.png create mode 100644 static/favicon.ico create mode 100755 static/flags/ad.png create mode 100755 static/flags/ae.png create mode 100755 static/flags/af.png create mode 100755 static/flags/ag.png create mode 100755 static/flags/ai.png create mode 100755 static/flags/al.png create mode 100755 static/flags/am.png create mode 100755 static/flags/an.png create mode 100755 static/flags/ao.png create mode 100755 static/flags/ar.png create mode 100755 static/flags/as.png create mode 100755 static/flags/at.png create mode 100755 static/flags/au.png create mode 100755 static/flags/aw.png create mode 100755 static/flags/ax.png create mode 100755 static/flags/az.png create mode 100755 static/flags/ba.png create mode 100755 static/flags/bb.png create mode 100755 static/flags/bd.png create mode 100755 static/flags/be.png create mode 100755 static/flags/bf.png create mode 100755 static/flags/bg.png create mode 100755 static/flags/bh.png create mode 100755 static/flags/bi.png create mode 100755 static/flags/bj.png create mode 100755 static/flags/bm.png create mode 100755 static/flags/bn.png create mode 100755 static/flags/bo.png create mode 100755 static/flags/br.png create mode 100755 static/flags/bs.png create mode 100755 static/flags/bt.png create mode 100755 static/flags/bv.png create mode 100755 static/flags/bw.png create mode 100755 static/flags/by.png create mode 100755 static/flags/bz.png create mode 100755 static/flags/ca.png create mode 100755 static/flags/cc.png create mode 100755 static/flags/cd.png create mode 100755 static/flags/cf.png create mode 100755 static/flags/cg.png create mode 100755 static/flags/ch.png create mode 100755 static/flags/ci.png create mode 100755 static/flags/ck.png create mode 100755 static/flags/cl.png create mode 100755 static/flags/cm.png create mode 100755 static/flags/cn.png create mode 100755 static/flags/co.png create mode 100755 static/flags/cr.png create mode 100755 static/flags/cs.png create mode 100755 static/flags/cu.png create mode 100755 static/flags/cv.png create mode 100755 static/flags/cx.png create mode 100755 static/flags/cy.png create mode 100755 static/flags/cz.png create mode 100755 static/flags/de.png create mode 100755 static/flags/dj.png create mode 100755 static/flags/dk.png create mode 100755 static/flags/dm.png create mode 100755 static/flags/do.png create mode 100755 static/flags/dz.png create mode 100755 static/flags/ec.png create mode 100755 static/flags/ee.png create mode 100755 static/flags/eg.png create mode 100755 static/flags/eh.png create mode 100755 static/flags/er.png create mode 100755 static/flags/es.png create mode 100755 static/flags/et.png create mode 100755 static/flags/fam.png create mode 100755 static/flags/fi.png create mode 100755 static/flags/fj.png create mode 100755 static/flags/fk.png create mode 100755 static/flags/fm.png create mode 100755 static/flags/fo.png create mode 100755 static/flags/fr.png create mode 100755 static/flags/ga.png create mode 100755 static/flags/gb.png create mode 100755 static/flags/gd.png create mode 100755 static/flags/ge.png create mode 100755 static/flags/gf.png create mode 100755 static/flags/gh.png create mode 100755 static/flags/gi.png create mode 100755 static/flags/gl.png create mode 100755 static/flags/gm.png create mode 100755 static/flags/gn.png create mode 100755 static/flags/gp.png create mode 100755 static/flags/gq.png create mode 100755 static/flags/gr.png create mode 100755 static/flags/gs.png create mode 100755 static/flags/gt.png create mode 100755 static/flags/gu.png create mode 100755 static/flags/gw.png create mode 100755 static/flags/gy.png create mode 100755 static/flags/hk.png create mode 100755 static/flags/hm.png create mode 100755 static/flags/hn.png create mode 100755 static/flags/hr.png create mode 100755 static/flags/ht.png create mode 100755 static/flags/hu.png create mode 100755 static/flags/id.png create mode 100755 static/flags/ie.png create mode 100755 static/flags/il.png create mode 100755 static/flags/in.png create mode 100755 static/flags/io.png create mode 100755 static/flags/iq.png create mode 100755 static/flags/ir.png create mode 100755 static/flags/is.png create mode 100755 static/flags/it.png create mode 100755 static/flags/jm.png create mode 100755 static/flags/jo.png create mode 100755 static/flags/jp.png create mode 100755 static/flags/ke.png create mode 100755 static/flags/kg.png create mode 100755 static/flags/kh.png create mode 100755 static/flags/ki.png create mode 100755 static/flags/km.png create mode 100755 static/flags/kn.png create mode 100755 static/flags/kp.png create mode 100755 static/flags/kr.png create mode 100755 static/flags/kw.png create mode 100755 static/flags/ky.png create mode 100755 static/flags/kz.png create mode 100755 static/flags/la.png create mode 100755 static/flags/lb.png create mode 100755 static/flags/lc.png create mode 100755 static/flags/li.png create mode 100755 static/flags/lk.png create mode 100755 static/flags/lr.png create mode 100755 static/flags/ls.png create mode 100755 static/flags/lt.png create mode 100755 static/flags/lu.png create mode 100755 static/flags/lv.png create mode 100755 static/flags/ly.png create mode 100755 static/flags/ma.png create mode 100755 static/flags/mc.png create mode 100755 static/flags/md.png create mode 100755 static/flags/me.png create mode 100755 static/flags/mg.png create mode 100755 static/flags/mh.png create mode 100755 static/flags/mk.png create mode 100755 static/flags/ml.png create mode 100755 static/flags/mm.png create mode 100755 static/flags/mn.png create mode 100755 static/flags/mo.png create mode 100755 static/flags/mp.png create mode 100755 static/flags/mq.png create mode 100755 static/flags/mr.png create mode 100755 static/flags/ms.png create mode 100755 static/flags/mt.png create mode 100755 static/flags/mu.png create mode 100755 static/flags/mv.png create mode 100755 static/flags/mw.png create mode 100755 static/flags/mx.png create mode 100755 static/flags/my.png create mode 100755 static/flags/mz.png create mode 100755 static/flags/na.png create mode 100755 static/flags/nc.png create mode 100755 static/flags/ne.png create mode 100755 static/flags/nf.png create mode 100755 static/flags/ng.png create mode 100755 static/flags/ni.png create mode 100755 static/flags/nl.png create mode 100755 static/flags/no.png create mode 100755 static/flags/np.png create mode 100755 static/flags/nr.png create mode 100755 static/flags/nu.png create mode 100755 static/flags/nz.png create mode 100755 static/flags/om.png create mode 100755 static/flags/pa.png create mode 100755 static/flags/pe.png create mode 100755 static/flags/pf.png create mode 100755 static/flags/pg.png create mode 100755 static/flags/ph.png create mode 100755 static/flags/pk.png create mode 100755 static/flags/pl.png create mode 100755 static/flags/pm.png create mode 100755 static/flags/pn.png create mode 100755 static/flags/pr.png create mode 100755 static/flags/ps.png create mode 100755 static/flags/pt.png create mode 100755 static/flags/pw.png create mode 100755 static/flags/py.png create mode 100755 static/flags/qa.png create mode 100755 static/flags/re.png create mode 100755 static/flags/ro.png create mode 100755 static/flags/rs.png create mode 100755 static/flags/ru.png create mode 100755 static/flags/rw.png create mode 100755 static/flags/sa.png create mode 100755 static/flags/sb.png create mode 100755 static/flags/sc.png create mode 100755 static/flags/sd.png create mode 100755 static/flags/se.png create mode 100755 static/flags/sg.png create mode 100755 static/flags/sh.png create mode 100755 static/flags/si.png create mode 100755 static/flags/sj.png create mode 100755 static/flags/sk.png create mode 100755 static/flags/sl.png create mode 100755 static/flags/sm.png create mode 100755 static/flags/sn.png create mode 100755 static/flags/so.png create mode 100755 static/flags/sr.png create mode 100755 static/flags/st.png create mode 100755 static/flags/sv.png create mode 100755 static/flags/sy.png create mode 100755 static/flags/sz.png create mode 100755 static/flags/tc.png create mode 100755 static/flags/td.png create mode 100755 static/flags/tf.png create mode 100755 static/flags/tg.png create mode 100755 static/flags/th.png create mode 100755 static/flags/tj.png create mode 100755 static/flags/tk.png create mode 100755 static/flags/tl.png create mode 100755 static/flags/tm.png create mode 100755 static/flags/tn.png create mode 100755 static/flags/to.png create mode 100755 static/flags/tr.png create mode 100755 static/flags/tt.png create mode 100755 static/flags/tv.png create mode 100755 static/flags/tw.png create mode 100755 static/flags/tz.png create mode 100755 static/flags/ua.png create mode 100755 static/flags/ug.png create mode 100755 static/flags/um.png create mode 100755 static/flags/us.png create mode 100755 static/flags/uy.png create mode 100755 static/flags/uz.png create mode 100755 static/flags/va.png create mode 100755 static/flags/vc.png create mode 100755 static/flags/ve.png create mode 100755 static/flags/vg.png create mode 100755 static/flags/vi.png create mode 100755 static/flags/vn.png create mode 100755 static/flags/vu.png create mode 100755 static/flags/wf.png create mode 100755 static/flags/ws.png create mode 100755 static/flags/xx.png create mode 100755 static/flags/ye.png create mode 100755 static/flags/yt.png create mode 100755 static/flags/za.png create mode 100755 static/flags/zm.png create mode 100755 static/flags/zw.png create mode 100755 static/icons/locked.png create mode 100755 static/icons/sticky.png create mode 100644 static/js/autofill.js create mode 100644 static/js/captcha.js create mode 100644 static/js/expand.js create mode 100644 static/js/hover.js create mode 100644 static/js/jquery.min.js create mode 100644 static/js/live.js create mode 100644 static/js/post-form.js create mode 100644 static/js/quote.js create mode 100644 static/js/time.js create mode 100755 static/spoiler.png create mode 100755 static/style.css create mode 100644 static/themes/yotsuba-b.css create mode 100644 static/themes/yotsuba.css create mode 100644 templates/action.html create mode 100644 templates/banned.html create mode 100755 templates/base.html create mode 100755 templates/board-catalog.html create mode 100755 templates/board.html create mode 100644 templates/edit-posts.html create mode 100755 templates/error.html create mode 100755 templates/index.html create mode 100644 templates/ip-posts.html create mode 100755 templates/login.html create mode 100644 templates/macros/board-links.html create mode 100644 templates/macros/catalog-entry.html create mode 100644 templates/macros/pagination.html create mode 100644 templates/macros/post-actions.html create mode 100644 templates/macros/post-form.html create mode 100644 templates/macros/post.html create mode 100644 templates/macros/staff-nav.html create mode 100644 templates/macros/static-pagination.html create mode 100644 templates/news.html create mode 100755 templates/overboard-catalog.html create mode 100644 templates/overboard.html create mode 100644 templates/page.html create mode 100644 templates/search.html create mode 100755 templates/staff/account.html create mode 100755 templates/staff/accounts.html create mode 100755 templates/staff/banners.html create mode 100755 templates/staff/bans.html create mode 100755 templates/staff/board-config.html create mode 100755 templates/staff/boards.html create mode 100644 templates/staff/edit-news.html create mode 100644 templates/staff/news.html create mode 100755 templates/staff/permissions.html create mode 100755 templates/staff/reports.html create mode 100644 templates/thread.html diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0c845ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/pages/*.html +/target +/templates_min +/uploads +Nekrochan.toml +.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..20f4884 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "html.validate.styles": false +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..d57f497 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3411 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb72882332b6d6282f428b77ba0358cb2687e61a6f6df6a6d3871e8a177c2d4f" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.3.3", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-files" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags 1.3.2", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.2", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.28", +] + +[[package]] +name = "actix-multipart" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2 0.4.9", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.4.9", + "time", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420b001bb709d8510c3e2659dae046e54509ff9528018d09c78381e765a1f9fa" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6f84b74db2535ebae81eede2f39b947dcbf01d093ae5f791e5dd414a1bf289" + +[[package]] +name = "askama" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47cbc3cf73fa8d9833727bbee4835ba5c421a0d65b72daf9a7b5d0e0f9cfb57e" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22fbe0413545c098358e56966ff22cdd039e10215ae213cfbd65032b119fc94" +dependencies = [ + "basic-toml", + "mime", + "mime_guess", + "nom", + "proc-macro2", + "quote", + "serde", + "syn 2.0.28", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838d03a705d72b12389b8930bd14cacf493be1380bfb15720d4d12db5ab03ac" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "captcha" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db21780337b425f968a2c3efa842eeaa4fe53d2bcb1eb27d2877460a862fb0ab" +dependencies = [ + "base64 0.13.1", + "hound", + "image", + "lodepng", + "rand", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "pure-rust-locales", + "serde", + "wasm-bindgen", + "windows-targets 0.48.1", +] + +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "const-oid" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cow-utils" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79bb3adfaf5f75d24b01aee375f7555907840fa2800e5ec8fa3b9e2031830173" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "educe" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079044df30bb07de7d846d41a184c4b00e66ebdac93ee459253474f3a47e50ae" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f76552f53cefc9a7f64987c3701b99d982f7690606fd67de1d09712fbf52f1" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible_collections" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "html-minifier" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8f62e1c8ee7bbdda853aebbbfb78b75a549a45ab031665a5da07a1579a1ad8" +dependencies = [ + "cow-utils", + "educe", + "html-escape", + "minifier", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +dependencies = [ + "base64 0.21.2", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lodepng" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ad39f75bbaa4b10bb6f2316543632a8046a5bcf9c785488d79720b21f044f8" +dependencies = [ + "crc32fast", + "fallible_collections", + "flate2", + "libc", + "rgb", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minifier" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95bbbf96b9ac3482c2a25450b67a15ed851319bc5fabf3b40742ea9066e84282" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nekrochan" +version = "0.1.0" +dependencies = [ + "actix", + "actix-files", + "actix-multipart", + "actix-web", + "actix-web-actors", + "anyhow", + "askama", + "captcha", + "chrono", + "chrono-tz", + "dotenv", + "encoding", + "enumflags2", + "env_logger", + "fs_extra", + "glob", + "html-minifier", + "ipnetwork", + "jsonwebtoken", + "lazy_static", + "log", + "num-traits", + "pwhash", + "rand", + "redis", + "regex", + "serde", + "serde_json", + "serde_qs", + "sha256", + "sqlx", + "thiserror", + "tokio", + "toml", + "uuid", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.1", +] + +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" + +[[package]] +name = "pem" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +dependencies = [ + "base64 0.21.2", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pure-rust-locales" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed02a829e62dc2715ceb8afb4f80e298148e1345749ceb369540fe0eb3368432" + +[[package]] +name = "pwhash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" +dependencies = [ + "blowfish", + "byteorder", + "hmac 0.10.1", + "md-5 0.9.1", + "rand", + "sha-1", + "sha2 0.9.9", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "serde", + "serde_json", + "sha1_smol", + "socket2 0.4.9", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha256" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a975c1bc0941703000eaf232c4d8ce188d8d5408d6344b6b2c8c6262772828" +dependencies = [ + "hex", + "sha2 0.10.7", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ef53c86d2066e04f0ac6b1364f16d13d82388e2d07f11a5c71782345555761" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a22fd81e9c1ad53c562edb869ff042b215d4eadefefc4784bacfbfd19835945" +dependencies = [ + "ahash 0.8.3", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "ipnetwork", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.7", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bb7c096a202b8164c175614cbfb79fe0e1e0a3d50e0374526183ef2974e4a2" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d644623ab9699014e5b3cb61a040d16caa50fd477008f63f1399ae35498a58" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.7", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264c59b28b6858796acfcedc660aa4c9075cc6e4ec8eb03cdca2a3e725726db" +dependencies = [ + "atoi", + "base64 0.21.2", + "bitflags 2.3.3", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5 0.10.5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab6147b81ca9213a7578f1b4c9d24c449a53953cd2222a7b5d7cd29a5c3139" +dependencies = [ + "atoi", + "base64 0.21.2", + "bitflags 2.3.3", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "ipnetwork", + "itoa", + "log", + "md-5 0.10.5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fba60afa64718104b71eec6984f8779d4caffff3b30cde91a75843c7efc126" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.3", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..c9aa539 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "nekrochan" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix = "0.13.3" +actix-files = "0.6.2" +actix-multipart = "0.6.0" +actix-web = { version = "4.3.1", features = ["cookies"] } +actix-web-actors = "4.3.0" +askama = "0.12.0" +anyhow = "1.0.71" +captcha = "0.0.9" +chrono = { version = "0.4.31", features = ["serde", "unstable-locales"] } +chrono-tz = "0.8.5" +dotenv = "0.15.0" +enumflags2 = "0.7.7" +encoding = "0.2.33" +env_logger = "0.11.2" +glob = "0.3.1" +ipnetwork = "0.20.0" +jsonwebtoken = "9.1.0" +lazy_static = "1.4.0" +log = "0.4.19" +num-traits = "0.2.16" +pwhash = "1.0.0" +rand = "0.8.5" +redis = { version = "0.24.0", features = ["aio", "json", "tokio-comp"] } +regex = "1.10.2" +serde = "1.0.166" +serde_json = "1.0.100" +serde_qs = "0.12.0" +sha256 = "1.1.4" +sqlx = { version = "0.7.0", features = [ + "runtime-tokio", + "postgres", + "json", + "chrono", + "ipnetwork", +] } +thiserror = "1.0.41" +tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] } +toml = "0.8.6" +uuid = { version = "1.7.0", features = ["v4"] } + +[build-dependencies] +anyhow = "1.0.74" +fs_extra = "1.3.0" +glob = "0.3.1" +html-minifier = "5.0.0" + +[profile.dev] +opt-level = 1 diff --git a/Nekrochan.toml.template b/Nekrochan.toml.template new file mode 100755 index 0000000..06f3fba --- /dev/null +++ b/Nekrochan.toml.template @@ -0,0 +1,47 @@ +[server] +port = ${PORT} +database_url = "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" +cache_url = "redis://${REDIS_HOST}/${REDIS_DB}" + +[site] +name = "${SITE_NAME}" +description = "${SITE_DESCRIPTION}" +theme = "yotsuba.css" +links = [] +noko = true + +[secrets] +auth_token = "${AUTH_SECRET}" +secure_trip = "${TRIP_SECRET}" +user_id = "${UID_SECRET}" + +[files] +videos = true +thumb_size = 150 +max_size_mb = 50 +max_height = 10000 +max_width = 10000 +cleanup_interval = 3600 + +[board_defaults] +anon_name = "Anonym" +page_size = 10 +page_count = 20 +file_limit = 1 +bump_limit = 500 +reply_limit = 1000 +locked = false +user_ids = false +flags = false +thread_captcha = "off" +reply_captcha = "off" +board_theme = "yotsuba.css" +require_thread_content = true +require_thread_file = true +require_reply_content = false +require_reply_file = false +antispam = true +antispam_ip = 5 +antispam_content = 10 +antispam_both = 60 +thread_cooldown = 60 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6431abe --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# czchan + +100% český imidžbórdový skript + +> 100% český přestože je kód anglicky... diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..7c682a8 --- /dev/null +++ b/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["templates_min"] diff --git a/build.rs b/build.rs new file mode 100755 index 0000000..0dcc6d4 --- /dev/null +++ b/build.rs @@ -0,0 +1,39 @@ +use anyhow::Error; +use fs_extra::dir::{copy, remove, CopyOptions}; +use glob::glob; +use html_minifier::minify; +use std::{ + fs::{read_to_string, File}, + io::Write, +}; + +fn main() -> Result<(), Error> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=migrations"); + println!("cargo:rerun-if-changed=templates"); + + remove("templates_min")?; + + copy( + "templates", + "templates_min", + &CopyOptions::new().copy_inside(true), + )?; + + let templates = glob("templates_min/**/*.html")?; + + for path in templates { + let path = path?; + + if !path.is_file() { + continue; + } + + let html = read_to_string(&path)?; + let minified = minify(html)?.replace('\n', "").replace(" ", " "); + + File::create(path)?.write_all(minified.as_bytes())?; + } + + Ok(()) +} diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..876170d --- /dev/null +++ b/configure.sh @@ -0,0 +1,68 @@ +set -e + +echo "# Výběr portu" +read -p "Port serveru [7000]: " port + +echo "# Konfigurace databáze" +read -p "Host databáze [localhost]: " db_host +read -p "Port databáze [5432]: " db_port + +read -p "Uživatelské jméno: " db_user +if [ "$db_user" == "" ] +then + echo "Uživatelské jméno je povinné" + exit 1 +fi + +read -p "Heslo: " db_password +if [ "$db_user" == "" ] +then + echo "Heslo je povinné" + exit 1 +fi + +read -p "Jméno databáze: " db_name +if [ "$db_name" == "" ] +then + echo "Jméno databáze je povinné" + exit 1 +fi + +echo "# Konfigurace redisu" +read -p "Host redisu [localhost]: " redis_host +read -p "Číslo databáze [0]: " redis_db + +echo "# Konfigurace stránky" + +read -p "Jméno stránky: " site_name +if [ "$site_name" == "" ] +then + echo "Jméno stránky je povinné" + exit 1 +fi + +read -p "Popis stránky: " site_description +if [ "$site_description" == "" ] +then + echo "Popis stránky je povinný" + exit 1 +fi + +export PORT=${port:-7000} +export DB_HOST=${db_host:-localhost} +export DB_PORT=${db_host:-5432} +export DB_USER=${db_user} +export DB_PASSWORD=${db_password} +export DB_NAME=${db_name} +export REDIS_HOST=${redis_host:-localhost} +export REDIS_DB=${redis_db:-0} +export REDIS_HOST=${redis_host:-localhost} +export SITE_NAME=${site_name} +export SITE_DESCRIPTION=${site_description} + +export AUTH_SECRET=`tr -dc A-Za-z0-9 Nekrochan.toml +mkdir -p ./uploads/thumb diff --git a/migrations/20230710121446_create_tables.sql b/migrations/20230710121446_create_tables.sql new file mode 100755 index 0000000..66e2632 --- /dev/null +++ b/migrations/20230710121446_create_tables.sql @@ -0,0 +1,28 @@ +CREATE TABLE accounts ( + username VARCHAR(32) NOT NULL PRIMARY KEY, + password VARCHAR(64) NOT NULL, + owner BOOLEAN NOT NULL DEFAULT false, + permissions JSONB NOT NULL DEFAULT '0'::jsonb, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE boards ( + id VARCHAR(16) NOT NULL PRIMARY KEY, + name VARCHAR(32) NOT NULL, + description VARCHAR(128) NOT NULL, + banners JSONB NOT NULL, + config JSONB NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE bans ( + id SERIAL NOT NULL PRIMARY KEY, + ip_range INET NOT NULL, + reason TEXT NOT NULL, + board VARCHAR(16) DEFAULT NULL REFERENCES boards(id), + issued_by VARCHAR(32) NOT NULL REFERENCES accounts(username), + appealable BOOLEAN NOT NULL DEFAULT true, + appeal TEXT DEFAULT NULL, + expires TIMESTAMPTZ DEFAULT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/20231216092451_global_banners.sql b/migrations/20231216092451_global_banners.sql new file mode 100644 index 0000000..14888f3 --- /dev/null +++ b/migrations/20231216092451_global_banners.sql @@ -0,0 +1,6 @@ +ALTER TABLE boards DROP COLUMN banners; + +CREATE TABLE banners ( + id SERIAL NOT NULL PRIMARY KEY, + banner JSONB NOT NULL +); diff --git a/migrations/20231217111814_create_news.sql b/migrations/20231217111814_create_news.sql new file mode 100644 index 0000000..18cb32d --- /dev/null +++ b/migrations/20231217111814_create_news.sql @@ -0,0 +1,8 @@ +CREATE TABLE news ( + id SERIAL NOT NULL PRIMARY KEY, + title VARCHAR(256) NOT NULL, + content TEXT NOT NULL, + content_nomarkup TEXT NOT NULL, + author VARCHAR(32) NOT NULL REFERENCES accounts(username), + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrations/20231229180942_remove_references.sql b/migrations/20231229180942_remove_references.sql new file mode 100644 index 0000000..5f125db --- /dev/null +++ b/migrations/20231229180942_remove_references.sql @@ -0,0 +1,2 @@ +ALTER TABLE bans DROP CONSTRAINT bans_issued_by_fkey; +ALTER TABLE news DROP CONSTRAINT news_author_fkey; diff --git a/src/auth.rs b/src/auth.rs new file mode 100755 index 0000000..5ced9fa --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,39 @@ +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +use crate::{ctx::Ctx, error::NekrochanError}; + +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub sub: String, +} + +impl Claims { + pub fn new(sub: String) -> Self { + Self { sub } + } + + pub fn encode(&self, ctx: &Ctx) -> Result { + let header = Header::default(); + let key = EncodingKey::from_secret(ctx.cfg.secrets.auth_token.as_bytes()); + + let auth = encode(&header, &self, &key)?; + + Ok(auth) + } + + pub fn decode(ctx: &Ctx, auth: &str) -> Result { + let key = DecodingKey::from_secret(ctx.cfg.secrets.auth_token.as_bytes()); + + let mut validation = Validation::default(); + validation.required_spec_claims = HashSet::from_iter(["sub".to_owned()]); + validation.validate_exp = false; + + let claims = decode(auth, &key, &validation) + .map_err(|_| NekrochanError::InvalidAuthError)? + .claims; + + Ok(claims) + } +} diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100755 index 0000000..f049f72 --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,79 @@ +use anyhow::Error; +use serde::{Deserialize, Serialize}; +use tokio::fs::read_to_string; + +#[derive(Deserialize, Debug, Clone)] +pub struct Cfg { + pub server: ServerCfg, + pub site: SiteCfg, + pub secrets: SecretsCfg, + pub files: FilesCfg, + pub board_defaults: BoardCfg, +} + +impl Cfg { + pub async fn load(path: &str) -> Result { + let cfg_string = read_to_string(path).await?; + let cfg: Cfg = toml::from_str(&cfg_string)?; + + Ok(cfg) + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ServerCfg { + pub port: u16, + pub database_url: String, + pub cache_url: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SiteCfg { + pub name: String, + pub description: String, + pub theme: String, + pub links: Vec>, + pub noko: bool, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SecretsCfg { + pub auth_token: String, + pub secure_trip: String, + pub user_id: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct FilesCfg { + pub videos: bool, + pub thumb_size: u32, + pub max_size_mb: usize, + pub max_height: u32, + pub max_width: u32, + pub cleanup_interval: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BoardCfg { + pub anon_name: String, + pub page_size: i64, + pub page_count: i64, + pub file_limit: usize, + pub bump_limit: i32, + pub reply_limit: i32, + pub locked: bool, + pub user_ids: bool, + pub flags: bool, + pub thread_captcha: String, + pub reply_captcha: String, + pub board_theme: String, + pub require_thread_content: bool, + pub require_thread_file: bool, + pub require_reply_content: bool, + pub require_reply_file: bool, + pub antispam: bool, + pub antispam_ip: i64, + pub antispam_content: i64, + pub antispam_both: i64, + pub thread_cooldown: i64, +} diff --git a/src/ctx.rs b/src/ctx.rs new file mode 100755 index 0000000..7fc518e --- /dev/null +++ b/src/ctx.rs @@ -0,0 +1,48 @@ +use actix::{Actor, Addr}; +use anyhow::Error; +use redis::{aio::MultiplexedConnection, Client}; +use sqlx::PgPool; +use std::net::SocketAddr; + +use crate::{cfg::Cfg, live_hub::LiveHub}; + +#[derive(Clone)] +pub struct Ctx { + pub cfg: Cfg, + db: PgPool, + cache: MultiplexedConnection, + hub: Addr, +} + +impl Ctx { + pub async fn new(cfg: Cfg) -> Result { + let db = PgPool::connect(&cfg.server.database_url).await?; + let client = Client::open(cfg.server.cache_url.as_str())?; + let cache = client.get_multiplexed_async_connection().await?; + let sync_cache = client.get_connection()?; + let hub = LiveHub::new(sync_cache).start(); + + Ok(Self { + cfg, + db, + cache, + hub, + }) + } + + pub fn bind_addr(&self) -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], self.cfg.server.port)) + } + + pub fn db(&self) -> &PgPool { + &self.db + } + + pub fn cache(&self) -> MultiplexedConnection { + self.cache.clone() + } + + pub fn hub(&self) -> Addr { + self.hub.clone() + } +} diff --git a/src/db/account.rs b/src/db/account.rs new file mode 100755 index 0000000..4b000e8 --- /dev/null +++ b/src/db/account.rs @@ -0,0 +1,117 @@ +use redis::{AsyncCommands, JsonAsyncCommands}; +use sqlx::{query, query_as, types::Json}; + +use super::models::Account; +use crate::{ctx::Ctx, error::NekrochanError, perms::PermissionWrapper}; + +impl Account { + pub async fn create( + ctx: &Ctx, + username: String, + password: String, + ) -> Result { + let account = + query_as("INSERT INTO accounts (username, password) VALUES ($1, $2) RETURNING *") + .bind(&username) + .bind(password) + .fetch_one(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{username}"), ".", &account) + .await?; + + Ok(account) + } + + pub async fn read(ctx: &Ctx, username: String) -> Result, NekrochanError> { + let account: Option = ctx + .cache() + .json_get(format!("accounts:{username}"), ".") + .await?; + + let account = match account { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(account) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let accounts = query_as("SELECT * FROM accounts ORDER BY owner DESC, created DESC") + .fetch_all(ctx.db()) + .await?; + + Ok(accounts) + } + + pub async fn update_password(&self, ctx: &Ctx, password: String) -> Result<(), NekrochanError> { + query("UPDATE accounts SET password = $1 WHERE username = $2") + .bind(&password) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{}", self.username), "password", &password) + .await?; + + Ok(()) + } + + pub async fn update_permissions( + &self, + ctx: &Ctx, + permissions: u64, + ) -> Result<(), NekrochanError> { + query("UPDATE accounts SET permissions = $1 WHERE username = $2") + .bind(Json(permissions)) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set( + format!("accounts:{}", self.username), + "permissions", + &permissions, + ) + .await?; + + Ok(()) + } + + pub async fn update_owner(&self, ctx: &Ctx, owner: bool) -> Result<(), NekrochanError> { + query("UPDATE accounts SET owner = $1 WHERE username = $2") + .bind(owner) + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("accounts:{}", self.username), "owner", &owner) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query("DELETE FROM accounts WHERE username = $1") + .bind(&self.username) + .execute(ctx.db()) + .await?; + + ctx.cache() + .del(format!("accounts:{}", self.username)) + .await?; + + Ok(()) + } +} + +impl Account { + pub fn perms(&self) -> PermissionWrapper { + PermissionWrapper::new(self.permissions.0, self.owner) + } +} diff --git a/src/db/ban.rs b/src/db/ban.rs new file mode 100755 index 0000000..3ae2a04 --- /dev/null +++ b/src/db/ban.rs @@ -0,0 +1,107 @@ +use chrono::{DateTime, Utc}; +use ipnetwork::IpNetwork; +use sqlx::{query, query_as}; +use std::{collections::HashMap, net::IpAddr}; + +use super::models::Ban; +use crate::{ctx::Ctx, error::NekrochanError}; + +impl Ban { + pub async fn create( + ctx: &Ctx, + account: String, + board: Option, + ip_range: IpNetwork, + reason: String, + appealable: bool, + expires: Option>, + ) -> Result { + let ban = query_as("INSERT INTO bans (ip_range, reason, board, issued_by, appealable, expires) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *") + .bind(ip_range) + .bind(reason) + .bind(board) + .bind(account) + .bind(appealable) + .bind(expires) + .fetch_one(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read(ctx: &Ctx, board: String, ip: IpAddr) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND (board = $1 OR board IS NULL) AND (ip_range >> $2 OR ip_range = $2)") + .bind(board) + .bind(ip) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_global(ctx: &Ctx, ip: IpNetwork) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND board IS NULL AND (ip_range >> $1 OR ip_range = $1)") + .bind(ip) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let bans = + query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) ORDER BY created DESC") + .fetch_all(ctx.db()) + .await?; + + Ok(bans) + } + + pub async fn read_by_id(ctx: &Ctx, id: i32) -> Result, NekrochanError> { + let ban = query_as("SELECT * FROM bans WHERE id = $1") + .bind(id) + .fetch_optional(ctx.db()) + .await?; + + Ok(ban) + } + + pub async fn read_by_ip( + ctx: &Ctx, + ip: IpAddr, + ) -> Result, Ban>, NekrochanError> { + let bans: Vec = query_as("SELECT * FROM bans WHERE (expires > CURRENT_TIMESTAMP OR expires IS NULL) AND (ip_range >> $1 OR ip_range = $1)") + .bind(ip) + .fetch_all(ctx.db()) + .await?; + + let mut ban_map = HashMap::new(); + + for ban in bans { + let board = ban.board.clone(); + + ban_map.insert(board, ban); + } + + Ok(ban_map) + } + + pub async fn update_appeal(&self, ctx: &Ctx, appeal: String) -> Result<(), NekrochanError> { + query("UPDATE bans SET appeal = $1 WHERE id = $2") + .bind(appeal) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query("DELETE FROM bans WHERE id = $1") + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } +} diff --git a/src/db/banner.rs b/src/db/banner.rs new file mode 100644 index 0000000..ab2b68f --- /dev/null +++ b/src/db/banner.rs @@ -0,0 +1,64 @@ +use redis::AsyncCommands; +use sqlx::{query, query_as, types::Json}; + +use super::models::{Banner, File}; +use crate::{ctx::Ctx, NekrochanError}; + +impl Banner { + pub async fn create(ctx: &Ctx, banner: File) -> Result { + let banner: Self = query_as("INSERT INTO banners (banner) VALUES ($1) RETURNING *") + .bind(Json(banner)) + .fetch_one(ctx.db()) + .await?; + + ctx.cache() + .zadd("banners", serde_json::to_string(&banner)?, banner.id) + .await?; + + Ok(banner) + } + + pub async fn read(ctx: &Ctx, id: i32) -> Result, NekrochanError> { + let banners: Vec = ctx.cache().zrangebyscore("banners", id, id).await?; + let json = banners.get(0); + + let banner = match json { + Some(json) => Some(serde_json::from_str(json)?), + None => None, + }; + + Ok(banner) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let banners_str: Vec = ctx.cache().zrange("banners", 0, -1).await?; + let banners_json = format!("[{}]", banners_str.join(",")); // If it works, it works + let banners = serde_json::from_str(&banners_json)?; + + Ok(banners) + } + + pub async fn read_random(ctx: &Ctx) -> Result, NekrochanError> { + let banner: Option = ctx.cache().zrandmember("banners", None).await?; + + let banner = match banner { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(banner) + } + + pub async fn remove(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + self.banner.delete().await; + + query("DELETE FROM banners WHERE id = $1") + .bind(self.id) + .execute(ctx.db()) + .await?; + + ctx.cache().zrembyscore("banners", self.id, self.id).await?; + + Ok(()) + } +} diff --git a/src/db/board.rs b/src/db/board.rs new file mode 100755 index 0000000..f701333 --- /dev/null +++ b/src/db/board.rs @@ -0,0 +1,249 @@ +use redis::{cmd, AsyncCommands, Connection, JsonAsyncCommands, JsonCommands}; +use sqlx::{query, query_as, types::Json}; +use std::collections::HashMap; + +use super::models::{Board, File}; +use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError}; + +impl Board { + pub async fn create( + ctx: &Ctx, + id: String, + name: String, + description: String, + ) -> Result { + let config = Json(ctx.cfg.board_defaults.clone()); + + let board: Board = query_as("INSERT INTO boards (id, name, description, config) VALUES ($1, $2, $3, $4) RETURNING *") + .bind(id) + .bind(name) + .bind(description) + .bind(config) + .fetch_one(ctx.db()) + .await?; + + query(&format!( + r#"CREATE TABLE posts_{} ( + id BIGSERIAL NOT NULL PRIMARY KEY, + board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id), + thread BIGINT DEFAULT NULL REFERENCES posts_{}(id), + name VARCHAR(32) NOT NULL, + user_id VARCHAR(6) NOT NULL DEFAULT '000000', + tripcode VARCHAR(12) DEFAULT NULL, + capcode VARCHAR(32) DEFAULT NULL, + email VARCHAR(256) DEFAULT NULL, + content TEXT NOT NULL, + content_nomarkup TEXT NOT NULL, + files JSONB NOT NULL, + password VARCHAR(64) DEFAULT NULL, + country VARCHAR(2) NOT NULL, + ip INET NOT NULL, + bumps INT NOT NULL DEFAULT 0, + replies INT NOT NULL DEFAULT 0, + quotes BIGINT[] NOT NULL DEFAULT '{{}}', + sticky BOOLEAN NOT NULL DEFAULT false, + locked BOOLEAN NOT NULL DEFAULT false, + reported TIMESTAMPTZ DEFAULT NULL, + reports JSONB NOT NULL DEFAULT '[]'::jsonb, + bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + )"#, + board.id, board.id, board.id + )) + .execute(ctx.db()) + .await?; + + ctx.cache() + .set(format!("board_threads:{}", board.id), 0) + .await?; + + ctx.cache().lpush("board_ids", &board.id).await?; + + ctx.cache() + .json_set(format!("boards:{}", board.id), ".", &board) + .await?; + + cmd("SORT") + .arg("board_ids") + .arg("ALPHA") + .arg("STORE") + .arg("board_ids") + .query_async(&mut ctx.cache()) + .await?; + + update_overboard(ctx, Self::read_all(ctx).await?).await?; + + Ok(board) + } + + pub async fn read(ctx: &Ctx, id: String) -> Result, NekrochanError> { + let board: Option = ctx.cache().json_get(format!("boards:{id}"), ".").await?; + + let board = match board { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(board) + } + + pub fn read_sync(cache: &mut Connection, id: String) -> Result, NekrochanError> { + let board: Option = cache.json_get(format!("boards:{id}"), ".")?; + + let board = match board { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(board) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let mut boards = Vec::new(); + let ids: Vec = ctx.cache().lrange("board_ids", 0, -1).await?; + + for id in ids { + if let Some(board) = Self::read(ctx, id).await? { + boards.push(board); + } + } + + Ok(boards) + } + + pub async fn read_all_map(ctx: &Ctx) -> Result, NekrochanError> { + let mut boards = HashMap::new(); + let ids: Vec = ctx.cache().lrange("board_ids", 0, -1).await?; + + for id in ids { + if let Some(board) = Self::read(ctx, id.clone()).await? { + boards.insert(id, board); + } + } + + Ok(boards) + } + + pub async fn read_post_count(&self, ctx: &Ctx) -> Result { + let (count,) = query_as(&format!("SELECT last_value FROM posts_{}_id_seq", self.id)) + .fetch_one(ctx.db()) + .await?; + + Ok(count) + } + + pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> { + query("UPDATE boards SET name = $1 WHERE id = $2") + .bind(&name) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "name", &name) + .await?; + + Ok(()) + } + + pub async fn update_description( + &self, + ctx: &Ctx, + description: String, + ) -> Result<(), NekrochanError> { + query("UPDATE boards SET description = $1 WHERE id = $2") + .bind(&description) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "description", &description) + .await?; + + Ok(()) + } + + pub async fn update_banners( + &self, + ctx: &Ctx, + banners: Vec, + ) -> Result<(), NekrochanError> { + query("UPDATE boards SET banners = $1 WHERE id = $2") + .bind(Json(&banners)) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "banners", &banners) + .await?; + + Ok(()) + } + + pub async fn update_config(&self, ctx: &Ctx, config: BoardCfg) -> Result<(), NekrochanError> { + query("UPDATE boards SET config = $1 WHERE id = $2") + .bind(Json(&config)) + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache() + .json_set(format!("boards:{}", self.id), "config", &config) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let boards = Self::read_all(ctx) + .await? + .into_iter() + .filter(|board| board.id != self.id) + .collect(); + + update_overboard(ctx, boards).await?; + + query("DELETE FROM bans WHERE board = $1") + .bind(&self.id) + .execute(ctx.db()) + .await?; + + query(&format!("DROP TABLE posts_{}", self.id)) + .execute(ctx.db()) + .await?; + + query("DELETE FROM boards WHERE id = $1") + .bind(&self.id) + .execute(ctx.db()) + .await?; + + ctx.cache().del(format!("boards:{}", self.id)).await?; + ctx.cache().lrem("board_ids", 0, &self.id).await?; + + Ok(()) + } +} + +async fn update_overboard(ctx: &Ctx, boards: Vec) -> Result<(), NekrochanError> { + query("DROP VIEW IF EXISTS overboard") + .execute(ctx.db()) + .await?; + + if boards.is_empty() { + return Ok(()); + } + + let unions = boards + .into_iter() + .map(|board| format!("SELECT * FROM posts_{}", board.id)) + .collect::>() + .join(" UNION "); + + query(&format!("CREATE VIEW overboard AS {unions}")) + .execute(ctx.db()) + .await?; + + Ok(()) +} diff --git a/src/db/cache.rs b/src/db/cache.rs new file mode 100644 index 0000000..b84caff --- /dev/null +++ b/src/db/cache.rs @@ -0,0 +1,100 @@ +use anyhow::Error; +use redis::{cmd, AsyncCommands, JsonAsyncCommands}; +use sha256::digest; +use sqlx::query_as; + +use super::models::{Account, Banner, Board, Post}; +use crate::ctx::Ctx; + +pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> { + cmd("FLUSHDB").query_async(&mut ctx.cache()).await?; + + let accounts: Vec = query_as("SELECT * FROM accounts") + .fetch_all(ctx.db()) + .await?; + + for account in &accounts { + ctx.cache() + .json_set(format!("accounts:{}", account.username), ".", &account) + .await?; + } + + let boards: Vec = query_as("SELECT * FROM boards").fetch_all(ctx.db()).await?; + + for board in &boards { + ctx.cache() + .json_set(format!("boards:{}", board.id), ".", board) + .await?; + + ctx.cache().lpush("board_ids", &board.id).await?; + } + + let banners: Vec = query_as("SELECT * FROM banners") + .fetch_all(ctx.db()) + .await?; + + for banner in &banners { + ctx.cache() + .zadd("banners", serde_json::to_string(banner)?, banner.id) + .await?; + } + + cmd("SORT") + .arg("board_ids") + .arg("ALPHA") + .arg("STORE") + .arg("board_ids") + .query_async(&mut ctx.cache()) + .await?; + + ctx.cache().set("total_threads", 0).await?; + + for board in &boards { + let (thread_count,): (i64,) = query_as(&format!( + "SELECT COUNT(id) FROM posts_{} WHERE thread IS NULL", + board.id + )) + .fetch_one(ctx.db()) + .await?; + + ctx.cache().incr("total_threads", thread_count).await?; + ctx.cache() + .set(format!("board_threads:{}", board.id), thread_count) + .await?; + } + + for board in &boards { + let posts = Post::read_all(ctx, board.id.clone()).await?; + + for post in posts { + let ip_key = format!("by_ip:{}", post.ip); + let content_key = format!( + "by_content:{}", + digest(post.content_nomarkup.to_lowercase()) + ); + + let member = format!("{}/{}", post.board, post.id); + let score = post.created.timestamp_micros(); + + ctx.cache().zadd(ip_key, &member, score).await?; + ctx.cache().zadd(content_key, &member, score).await?; + + if post.thread.is_none() { + let key = format!("last_thread:{}", post.ip); + let last_thread = ctx + .cache() + .get::<_, Option>(&key) + .await? + .unwrap_or_default(); + + let timestamp = post.created.timestamp_micros(); + + if timestamp > last_thread { + ctx.cache().set(key, timestamp).await?; + } + } + } + } + + Ok(()) +} diff --git a/src/db/local_stats.rs b/src/db/local_stats.rs new file mode 100644 index 0000000..136ee99 --- /dev/null +++ b/src/db/local_stats.rs @@ -0,0 +1,30 @@ +use sqlx::query_as; + +use super::models::LocalStats; +use crate::{ctx::Ctx, error::NekrochanError}; + +impl LocalStats { + pub async fn read(ctx: &Ctx) -> Result { + let (post_count,) = query_as( + "SELECT COALESCE(SUM(last_value)::bigint, 0) FROM pg_sequences WHERE sequencename LIKE 'posts_%_id_seq'", + ) + .fetch_one(ctx.db()) + .await?; + + let (file_count, file_size) = query_as( + r#"SELECT COUNT(files), COALESCE(SUM((files->>'size')::bigint)::bigint, 0) FROM ( + SELECT jsonb_array_elements(files) AS files FROM overboard + ) flatten"#, + ) + .fetch_one(ctx.db()) + .await?; + + let stats = Self { + post_count, + file_count, + file_size, + }; + + Ok(stats) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100755 index 0000000..2bbd7e5 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,10 @@ +pub mod cache; +pub mod models; + +mod account; +mod ban; +mod banner; +mod board; +mod local_stats; +mod newspost; +mod post; diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100755 index 0000000..797ce19 --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,107 @@ +use std::net::IpAddr; + +use chrono::{DateTime, Utc}; +use ipnetwork::IpNetwork; +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json, FromRow}; + +use crate::cfg::BoardCfg; + +#[derive(FromRow, Serialize, Deserialize, Clone)] +pub struct Account { + pub username: String, + pub password: String, + pub owner: bool, + pub permissions: Json, + pub created: DateTime, +} + +#[derive(FromRow, Serialize, Deserialize, Clone)] +pub struct Board { + pub id: String, + pub name: String, + pub description: String, + pub config: Json, + pub created: DateTime, +} + +#[derive(FromRow, Serialize, Deserialize, Clone)] +pub struct Ban { + pub id: i32, + pub ip_range: IpNetwork, + pub reason: String, + pub board: Option, + pub issued_by: String, + pub appealable: bool, + pub appeal: Option, + pub expires: Option>, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Report { + pub reason: String, + pub reporter_country: String, + pub reporter_ip: IpAddr, +} + +#[derive(FromRow, Serialize, Deserialize, Clone)] +pub struct Post { + pub id: i64, + pub board: String, + pub thread: Option, + pub name: String, + pub user_id: String, + pub tripcode: Option, + pub capcode: Option, + pub email: Option, + pub content: String, + pub content_nomarkup: String, + pub files: Json>, + pub password: String, + pub country: String, + pub ip: IpAddr, + pub bumps: i32, + pub replies: i32, + pub quotes: Vec, + pub sticky: bool, + pub locked: bool, + pub reported: Option>, + pub reports: Json>, + pub bumped: DateTime, + pub created: DateTime, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Banner { + pub id: i32, + pub banner: Json, +} + +#[derive(FromRow)] +pub struct NewsPost { + pub id: i32, + pub title: String, + pub content: String, + pub content_nomarkup: String, + pub author: String, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct File { + pub original_name: String, + pub format: String, + pub thumb_format: Option, + pub spoiler: bool, + pub width: u32, + pub height: u32, + pub timestamp: i64, + pub size: usize, +} + +pub struct LocalStats { + pub post_count: i64, + pub file_count: i64, + pub file_size: i64, +} diff --git a/src/db/newspost.rs b/src/db/newspost.rs new file mode 100644 index 0000000..93b2417 --- /dev/null +++ b/src/db/newspost.rs @@ -0,0 +1,74 @@ +use sqlx::{query, query_as}; + +use super::models::NewsPost; +use crate::{ctx::Ctx, error::NekrochanError}; + +impl NewsPost { + pub async fn create( + ctx: &Ctx, + title: String, + content: String, + content_nomarkup: String, + author: String, + ) -> Result { + let newspost = query_as("INSERT INTO news (title, content, content_nomarkup, author) VALUES ($1, $2, $3, $4) RETURNING *") + .bind(title) + .bind(content) + .bind(content_nomarkup) + .bind(author) + .fetch_one(ctx.db()) + .await?; + + Ok(newspost) + } + + pub async fn read(ctx: &Ctx, id: i32) -> Result, NekrochanError> { + let newspost = query_as("SELECT * FROM news WHERE id = $1") + .bind(id) + .fetch_optional(ctx.db()) + .await?; + + Ok(newspost) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let newsposts = query_as("SELECT * FROM news ORDER BY created DESC") + .fetch_all(ctx.db()) + .await?; + + Ok(newsposts) + } + + pub async fn read_latest(ctx: &Ctx) -> Result, NekrochanError> { + let newspost = query_as("SELECT * FROM news ORDER BY created DESC LIMIT 1") + .fetch_optional(ctx.db()) + .await?; + + Ok(newspost) + } + + pub async fn update( + &self, + ctx: &Ctx, + content: String, + content_nomarkup: String, + ) -> Result<(), NekrochanError> { + query("UPDATE news SET content = $1, content_nomarkup = $2 WHERE id = $3") + .bind(content) + .bind(content_nomarkup) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query("DELETE FROM news WHERE id = $1") + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } +} diff --git a/src/db/post.rs b/src/db/post.rs new file mode 100755 index 0000000..8dfd198 --- /dev/null +++ b/src/db/post.rs @@ -0,0 +1,594 @@ +use chrono::Utc; +use redis::AsyncCommands; +use sha256::digest; +use sqlx::{query, query_as, types::Json}; +use std::net::IpAddr; + +use super::models::{Board, File, Post, Report}; +use crate::{ + ctx::Ctx, + error::NekrochanError, + live_hub::{PostCreatedMessage, PostRemovedMessage, PostUpdatedMessage}, + GENERIC_PAGE_SIZE, +}; + +impl Post { + #[allow(clippy::too_many_arguments)] + pub async fn create( + ctx: &Ctx, + board: &Board, + thread: Option, + name: String, + tripcode: Option, + capcode: Option, + email: Option, + content: String, + content_nomarkup: String, + files: Vec, + password: String, + country: String, + ip: IpAddr, + bump: bool, + ) -> Result { + let post: Post = query_as(&format!( + r#"INSERT INTO posts_{} + (thread, name, tripcode, capcode, email, content, content_nomarkup, files, password, country, ip) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING *"#, board.id) + ) + .bind(thread) + .bind(name) + .bind(tripcode) + .bind(capcode) + .bind(email) + .bind(content) + .bind(content_nomarkup) + .bind(Json(files)) + .bind(password) + .bind(country) + .bind(ip) + .fetch_one(ctx.db()) + .await?; + + if let Some(thread) = thread { + query(&format!( + "UPDATE posts_{} SET replies = replies + 1 WHERE id = $1", + board.id + )) + .bind(thread) + .execute(ctx.db()) + .await?; + + if bump { + query(&format!( + "UPDATE posts_{} SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE id = $1", + board.id + )) + .bind(thread) + .execute(ctx.db()) + .await?; + } + } else { + delete_old_threads(ctx, board).await?; + + ctx.cache().incr("total_threads", 1).await?; + ctx.cache() + .incr(format!("board_threads:{}", board.id), 1) + .await?; + } + + let ip_key = format!("by_ip:{ip}"); + let content_key = format!( + "by_content:{}", + digest(post.content_nomarkup.to_lowercase()) + ); + + let member = format!("{}/{}", board.id, post.id); + let score = post.created.timestamp_micros(); + + ctx.cache().zadd(ip_key, &member, score).await?; + ctx.cache().zadd(content_key, &member, score).await?; + + if thread.is_none() { + ctx.cache() + .set(format!("last_thread:{ip}"), post.created.timestamp_micros()) + .await?; + } + + Ok(post) + } + + pub async fn create_report( + &self, + ctx: &Ctx, + reason: String, + reporter_country: String, + reporter_ip: IpAddr, + ) -> Result<(), NekrochanError> { + let mut reports = self.reports.clone(); + + reports.push(Report { + reason, + reporter_country, + reporter_ip, + }); + + query(&format!( + "UPDATE posts_{} SET reported = CURRENT_TIMESTAMP, reports = $1 WHERE id = $2", + self.board + )) + .bind(reports) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } + + pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result, NekrochanError> { + let post = query_as(&format!("SELECT * FROM posts_{} WHERE id = $1", board)) + .bind(id) + .fetch_optional(ctx.db()) + .await?; + + Ok(post) + } + + pub async fn read_board_page( + ctx: &Ctx, + board: &Board, + page: i64, + ) -> Result, NekrochanError> { + let posts = query_as(&format!( + r#"SELECT * FROM posts_{} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC + LIMIT $1 + OFFSET $2"#, + board.id + )) + .bind(board.config.0.page_size) + .bind((page - 1) * board.config.0.page_size) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_board_catalog(ctx: &Ctx, board: String) -> Result, NekrochanError> { + let posts = query_as(&format!( + r#"SELECT * FROM posts_{board} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC"# + )) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_overboard_page(ctx: &Ctx, page: i64) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE thread IS NULL + ORDER BY bumped DESC + LIMIT $1 + OFFSET $2"#, + ) + .bind(GENERIC_PAGE_SIZE) + .bind((page - 1) * GENERIC_PAGE_SIZE) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_overboard_catalog(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE thread IS NULL + ORDER BY bumped DESC"#, + ) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_reports_page(ctx: &Ctx, page: i64) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE reports != '[]'::jsonb + ORDER BY jsonb_array_length(reports), reported DESC + LIMIT $1 + OFFSET $2"#, + ) + .bind(GENERIC_PAGE_SIZE) + .bind((page - 1) * GENERIC_PAGE_SIZE) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_ip_page( + ctx: &Ctx, + ip: IpAddr, + page: i64, + ) -> Result, NekrochanError> { + let posts = query_as( + r#"SELECT * FROM overboard + WHERE ip = $1 + ORDER BY created DESC + LIMIT $2 + OFFSET $3"#, + ) + .bind(ip) + .bind(GENERIC_PAGE_SIZE) + .bind((page - 1) * GENERIC_PAGE_SIZE) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_replies(&self, ctx: &Ctx) -> Result, NekrochanError> { + let replies = query_as(&format!( + "SELECT * FROM posts_{} WHERE thread = $1 ORDER BY sticky DESC, created ASC", + self.board + )) + .bind(self.id) + .fetch_all(ctx.db()) + .await?; + + Ok(replies) + } + + pub async fn read_replies_after( + &self, + ctx: &Ctx, + last: i64, + ) -> Result, NekrochanError> { + let replies = query_as(&format!( + "SELECT * FROM posts_{} WHERE thread = $1 AND id > $2 ORDER BY created ASC", + self.board + )) + .bind(self.id) + .bind(last) + .fetch_all(ctx.db()) + .await?; + + Ok(replies) + } + + pub async fn read_all(ctx: &Ctx, board: String) -> Result, NekrochanError> { + let posts = query_as(&format!("SELECT * FROM posts_{board}")) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_all_overboard(ctx: &Ctx) -> Result, NekrochanError> { + let posts = query_as("SELECT * FROM overboard") + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_by_query( + ctx: &Ctx, + board: &Board, + query: String, + page: i64, + ) -> Result, NekrochanError> { + let posts = query_as(&format!( + "SELECT * FROM posts_{} WHERE LOWER(content_nomarkup) LIKE LOWER($1) ORDER BY created DESC LIMIT $2 OFFSET $3", + board.id + )) + .bind(format!("%{query}%")) + .bind(board.config.0.page_size) + .bind((page - 1) * board.config.0.page_size) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn read_by_query_overboard( + ctx: &Ctx, + query: String, + page: i64, + ) -> Result, NekrochanError> { + let posts = + query_as("SELECT * FROM overboard WHERE LOWER(content_nomarkup) LIKE LOWER($1) ORDER BY created DESC LIMIT $2 OFFSET $3") + .bind(format!("%{query}%")) + .bind(GENERIC_PAGE_SIZE) + .bind((page - 1) * GENERIC_PAGE_SIZE) + .fetch_all(ctx.db()) + .await?; + + Ok(posts) + } + + pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> { + let post = query_as(&format!( + "UPDATE posts_{} SET user_id = $1 WHERE id = $2 RETURNING *", + self.board, + )) + .bind(user_id) + .bind(self.id) + .fetch_one(ctx.db()) + .await?; + + ctx.hub().send(PostCreatedMessage { post }).await?; + + Ok(()) + } + + pub async fn update_sticky(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let post = query_as(&format!( + "UPDATE posts_{} SET sticky = NOT sticky WHERE id = $1 RETURNING *", + self.board + )) + .bind(self.id) + .fetch_one(ctx.db()) + .await?; + + ctx.hub().send(PostUpdatedMessage { post }).await?; + + Ok(()) + } + + pub async fn update_lock(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let post = query_as(&format!( + "UPDATE posts_{} SET locked = NOT locked WHERE id = $1 RETURNING *", + self.board + )) + .bind(self.id) + .fetch_one(ctx.db()) + .await?; + + ctx.hub().send(PostUpdatedMessage { post }).await?; + + Ok(()) + } + + pub async fn update_content( + &self, + ctx: &Ctx, + content: String, + content_nomarkup: String, + ) -> Result<(), NekrochanError> { + let post = query_as(&format!( + "UPDATE posts_{} SET content = $1, content_nomarkup = $2 WHERE id = $3 RETURNING *", + self.board + )) + .bind(content) + .bind(&content_nomarkup) + .bind(self.id) + .fetch_optional(ctx.db()) + .await?; + + let Some(post) = post else { return Ok(()) }; + + let old_key = format!( + "by_content:{}", + digest(self.content_nomarkup.to_lowercase()) + ); + let new_key = format!("by_content:{}", digest(content_nomarkup.to_lowercase())); + let member = format!("{}/{}", self.board, self.id); + let score = Utc::now().timestamp_micros(); + + ctx.cache().zrem(old_key, &member).await?; + ctx.cache().zadd(new_key, &member, score).await?; + ctx.hub().send(PostUpdatedMessage { post }).await?; + + Ok(()) + } + + pub async fn update_quotes(&self, ctx: &Ctx, id: i64) -> Result<(), NekrochanError> { + let post = query_as(&format!( + "UPDATE posts_{} SET quotes = array_append(quotes, $1) WHERE id = $2 RETURNING *", + self.board + )) + .bind(id) + .bind(self.id) + .fetch_one(ctx.db()) + .await?; + + ctx.hub().send(PostUpdatedMessage { post }).await?; + + Ok(()) + } + + pub async fn update_spoiler(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let mut files = self.files.clone(); + + for file in files.iter_mut() { + file.spoiler = !file.spoiler; + } + + let post = query_as(&format!( + "UPDATE posts_{} SET files = $1 WHERE id = $2 RETURNING *", + self.board + )) + .bind(Json(files)) + .bind(self.id) + .fetch_one(ctx.db()) + .await?; + + ctx.hub().send(PostUpdatedMessage { post }).await?; + + Ok(()) + } + + pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + let to_be_deleted: Vec = query_as(&format!( + "SELECT * FROM posts_{} WHERE id = $1 OR thread = $1 ORDER BY id ASC", + self.board + )) + .bind(self.id) + .fetch_all(ctx.db()) + .await?; + + for post in &to_be_deleted { + for file in post.files.iter() { + file.delete().await; + } + + let id = post.id; + let url = post.post_url(); + + let live_quote = format!(">>{id}"); + let dead_quote = format!(">>{id}"); + + let posts = query_as(&format!( + "UPDATE posts_{} SET content = REPLACE(content, $1, $2) WHERE content LIKE '%{}%' RETURNING *", + self.board, live_quote + )) + .bind(live_quote) + .bind(dead_quote) + .fetch_all(ctx.db()) + .await?; + + for post in posts { + ctx.hub().send(PostUpdatedMessage { post }).await?; + } + + let posts = query_as(&format!( + "UPDATE posts_{} SET quotes = array_remove(quotes, $1) WHERE $1 = ANY(quotes) RETURNING *", + self.board + )) + .bind(id) + .fetch_all(ctx.db()) + .await?; + + for post in posts { + ctx.hub().send(PostUpdatedMessage { post }).await?; + } + + let ip_key = format!("by_ip:{}", post.ip); + let content_key = format!( + "by_content:{}", + digest(post.content_nomarkup.to_lowercase()) + ); + + let member = format!("{}/{}", post.board, post.id); + + ctx.cache().zrem(ip_key, &member).await?; + ctx.cache().zrem(content_key, &member).await?; + ctx.hub() + .send(PostRemovedMessage { post: post.clone() }) + .await?; + } + + let in_list = to_be_deleted + .iter() + .map(|post| (post.id)) + .collect::>(); + + query(&format!( + "DELETE FROM posts_{} WHERE id = ANY($1)", + self.board + )) + .bind(&in_list) + .execute(ctx.db()) + .await?; + + if let Some(thread) = self.thread { + query(&format!( + "UPDATE posts_{} SET replies = replies - 1 WHERE id = $1", + self.board + )) + .bind(thread) + .execute(ctx.db()) + .await?; + } else { + ctx.cache().decr("total_threads", 1).await?; + ctx.cache() + .decr(format!("board_threads:{}", self.board), 1) + .await?; + } + + Ok(()) + } + + pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + for file in &self.files.0 { + file.delete().await; + } + + query(&format!( + "UPDATE posts_{} SET files = '[]'::jsonb WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + ctx.hub() + .send(PostUpdatedMessage { post: self.clone() }) + .await?; + + Ok(()) + } + + pub async fn delete_reports(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + query(&format!( + "UPDATE posts_{} SET reported = NULL, reports = '[]'::jsonb WHERE id = $1", + self.board + )) + .bind(self.id) + .execute(ctx.db()) + .await?; + + Ok(()) + } +} + +impl Post { + pub fn post_url(&self) -> String { + format!( + "/boards/{}/{}#{}", + self.board, + self.thread.unwrap_or(self.id), + self.id + ) + } + + pub fn post_url_notarget(&self) -> String { + format!("/boards/{}/{}", self.board, self.id) + } + + pub fn thread_url(&self) -> String { + format!("/boards/{}/{}", self.board, self.thread.unwrap_or(self.id),) + } +} + +async fn delete_old_threads(ctx: &Ctx, board: &Board) -> Result<(), NekrochanError> { + let old_threads: Vec = query_as(&format!( + r#"SELECT * FROM posts_{} + WHERE thread IS NULL AND id NOT IN ( + SELECT id + FROM ( + SELECT id + FROM posts_{} + WHERE thread IS NULL + ORDER BY sticky DESC, bumped DESC + LIMIT $1 + ) catty + )"#, + board.id, board.id + )) + .bind(board.config.0.page_size * board.config.0.page_count) + .fetch_all(ctx.db()) + .await?; + + for thread in &old_threads { + thread.delete(ctx).await?; + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100755 index 0000000..97f4b5e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,269 @@ +use actix_web::{http::StatusCode, ResponseError}; +use log::error; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NekrochanError { + #[error("Účet '{}' neexistuje.", .0)] + AccountNotFound(String), + #[error("Tento ban už byl odvolán.")] + AlreadyAppealedError, + #[error("Odvolání můsí mít 1-1000 znaků.")] + BanAppealFormatError, + #[error("Žádný takový ban pro tuto IP adresu neexistuje.")] + BanNotFound, + #[error("Důvod banu musí mít 1-200 znaků.")] + BanReasonFormatError, + #[error("Nástěnka /{}/ je uzamčená.", .0)] + BoardLockError(String), + #[error("Jméno nástěnky musí mít 1-32 znaků.")] + BoardNameFormatError, + #[error("Nástěnka /{}/ neexistuje.", .0)] + BoardNotFound(String), + #[error("Capcode nesmí mít více než 32 znaků.")] + CapcodeFormatError, + #[error("Obsah nesmí mít více než 10000 znaků.")] + ContentFormatError, + #[error("Popis nesmí mít více než 128 znaků.")] + DescriptionFormatError, + #[error("E-mail nesmí mít více než 256 znaků.")] + EmailFormatError, + #[error("Příspěvek musí mít obsah nebo soubor.")] + EmptyPostError, + #[error("Chyba při zpracovávání souboru '{}': {}", .0, .1)] + FileError(String, &'static str), + #[error("Maximální počet souborů na této nástěnce je {}.", .0)] + FileLimitError(usize), + #[error("Tvůj příspěvek vypadá jako spam.")] + FloodError, + #[error("Domovní stránka vznikne po vytvoření nástěnky.")] + HomePageError, + #[error("ID musí mít 1-16 znaků a obsahovat pouze alfanumerické znaky.")] + IdFormatError, + #[error("Nesprávné řešení CAPTCHA.")] + IncorrectCaptchaError, + #[error("Nesprávné přihlašovací údaje.")] + IncorrectCredentialError, + #[error("Nesprávné heslo pro příspěvek #{}.", .0)] + IncorrectPasswordError(i64), + #[error("Nedostatečná oprávnění.")] + InsufficientPermissionError, + #[error("Server se připojil k 41 procentům.")] + InternalError, + #[error("Neplatný autentizační token. Vymaž soubory cookie.")] + InvalidAuthError, + #[error("Tato CAPTCHA vypršela nebo neexistuje.")] + InvalidCaptchaError, + #[error("Neplatná strana.")] + InvalidPageError, + #[error("Tento příspěvek není vlákno.")] + IsReplyError, + #[error("Obsah musí mít 1-20000 znaků.")] + NewsContentFormatError, + #[error("Titulek musí mít 1-100 znaků.")] + NewsTitleFormatError, + #[error("Tato nástěnka nevyžaduje CAPTCHA.")] + NoCaptchaError, + #[error("Příspěvek musí mít obsah.")] + NoContentError, + #[error("Příspěvek musí mít soubor.")] + NoFileError, + #[error("Nebyly vybrány žádné příspěvky.")] + NoPostsError, + #[error("Pro přístup se musíš přihlásit.")] + NotLoggedInError, + #[error("Nadnástěnka nebyla inicializována.")] + OverboardError, + #[error("Účet vlastníka nemůže být vymazán.")] + OwnerDeletionError, + #[error("Stránka {} neexistuje", .0)] + PageNotFound(String), + #[error("Heslo musí mít alespoň 8 znaků.")] + PasswordFormatError, + #[error("Jméno nesmí mít více než 32 znaků.")] + PostNameFormatError, + #[error("Příspěvek /{}/{} neexistuje.", .0, .1)] + PostNotFound(String, i64), + #[error("Hledaný termín musí mít 1-256 znaků.")] + QueryFormatError, + #[error("Vlákno dosáhlo limitu odpovědí.")] + ReplyLimitError, + #[error("Hlášení můsí mít 1-200 znaků.")] + ReportFormatError, + #[error("Na této nástěnce se musí vyplnit CAPTCHA.")] + RequiredCaptchaError, + #[error("Toto vlákno je uzamčené.")] + ThreadLockError, + #[error("Tento ban nelze odvolat.")] + UnappealableError, + #[error("Uživatelské jméno musí mít 1-32 znaků.")] + UsernameFormatError, +} + +impl From for NekrochanError { + fn from(e: actix::MailboxError) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: actix_web::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: askama::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: ipnetwork::IpNetworkError) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: jsonwebtoken::errors::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: pwhash::error::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: regex::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: redis::RedisError) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: serde_json::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: serde_qs::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: sqlx::Error) -> Self { + let overboard_err = match e.as_database_error() { + Some(e) => e.message() == "relation \"overboard\" does not exist", + None => false, + }; + + if overboard_err { + Self::OverboardError + } else { + error!("{e:#?}"); + Self::InternalError + } + } +} + +impl From for NekrochanError { + fn from(e: std::io::Error) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: std::net::AddrParseError) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl From> for NekrochanError { + fn from(_: std::sync::PoisonError) -> Self { + error!("Some Mutex or Lock got poisoned or something"); + Self::InternalError + } +} + +impl From for NekrochanError { + fn from(e: tokio::task::JoinError) -> Self { + error!("Internal server error: {e:#?}"); + Self::InternalError + } +} + +impl ResponseError for NekrochanError { + fn status_code(&self) -> StatusCode { + match self { + NekrochanError::AccountNotFound(_) => StatusCode::NOT_FOUND, + NekrochanError::AlreadyAppealedError => StatusCode::BAD_REQUEST, + NekrochanError::BanAppealFormatError => StatusCode::BAD_REQUEST, + NekrochanError::BanNotFound => StatusCode::NOT_FOUND, + NekrochanError::BanReasonFormatError => StatusCode::BAD_REQUEST, + NekrochanError::BoardLockError(_) => StatusCode::FORBIDDEN, + NekrochanError::BoardNameFormatError => StatusCode::BAD_REQUEST, + NekrochanError::BoardNotFound(_) => StatusCode::NOT_FOUND, + NekrochanError::CapcodeFormatError => StatusCode::BAD_REQUEST, + NekrochanError::ContentFormatError => StatusCode::BAD_REQUEST, + NekrochanError::DescriptionFormatError => StatusCode::BAD_REQUEST, + NekrochanError::EmailFormatError => StatusCode::BAD_REQUEST, + NekrochanError::EmptyPostError => StatusCode::BAD_REQUEST, + NekrochanError::FileError(_, _) => StatusCode::UNPROCESSABLE_ENTITY, + NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST, + NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS, + NekrochanError::HomePageError => StatusCode::NOT_FOUND, + NekrochanError::IdFormatError => StatusCode::BAD_REQUEST, + NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED, + NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED, + NekrochanError::IncorrectPasswordError(_) => StatusCode::UNAUTHORIZED, + NekrochanError::InsufficientPermissionError => StatusCode::FORBIDDEN, + NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED, + NekrochanError::InvalidCaptchaError => StatusCode::BAD_REQUEST, + NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST, + NekrochanError::IsReplyError => StatusCode::BAD_REQUEST, + NekrochanError::NewsContentFormatError => StatusCode::BAD_REQUEST, + NekrochanError::NewsTitleFormatError => StatusCode::BAD_REQUEST, + NekrochanError::NoCaptchaError => StatusCode::NOT_FOUND, + NekrochanError::NoContentError => StatusCode::BAD_REQUEST, + NekrochanError::NoFileError => StatusCode::BAD_REQUEST, + NekrochanError::NoPostsError => StatusCode::BAD_REQUEST, + NekrochanError::NotLoggedInError => StatusCode::UNAUTHORIZED, + NekrochanError::OverboardError => StatusCode::INTERNAL_SERVER_ERROR, + NekrochanError::OwnerDeletionError => StatusCode::FORBIDDEN, + NekrochanError::PageNotFound(_) => StatusCode::NOT_FOUND, + NekrochanError::PasswordFormatError => StatusCode::BAD_REQUEST, + NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST, + NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND, + NekrochanError::QueryFormatError => StatusCode::BAD_REQUEST, + NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN, + NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST, + NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED, + NekrochanError::ThreadLockError => StatusCode::FORBIDDEN, + NekrochanError::UnappealableError => StatusCode::BAD_REQUEST, + NekrochanError::UsernameFormatError => StatusCode::BAD_REQUEST, + } + } +} diff --git a/src/files.rs b/src/files.rs new file mode 100755 index 0000000..a3b9c98 --- /dev/null +++ b/src/files.rs @@ -0,0 +1,299 @@ +use actix_multipart::form::tempfile::TempFile; +use chrono::Utc; +use std::process::Command; +use tokio::{ + fs::{copy, remove_file}, + task::spawn_blocking, +}; + +use crate::{cfg::Cfg, db::models::File, error::NekrochanError}; + +impl File { + pub async fn new( + cfg: &Cfg, + temp_file: TempFile, + spoiler: bool, + thumb: bool, + ) -> Result { + let original_name = temp_file.file_name.unwrap_or_else(|| "unknown".into()); + + let mime = temp_file + .content_type + .ok_or(NekrochanError::FileError( + original_name.clone(), + "žádný mime typ", + ))? + .to_string(); + + let (video, format) = match mime.as_str() { + "image/jpeg" => (false, "jpg"), + "image/pjpeg" => (false, "jpg"), + "image/png" => (false, "png"), + "image/bmp" => (false, "bmp"), + "image/gif" => (false, "gif"), + "image/webp" => (false, "webp"), + "image/apng" => (false, "apng"), + "video/mpeg" => (true, "mpeg"), + "video/quicktime" => (true, "mov"), + "video/mp4" => (true, "mp4"), + "video/webm" => (true, "webm"), + "video/x-matroska" => (true, "mkv"), + "video/ogg" => (true, "ogg"), + _ => { + return Err(NekrochanError::FileError( + original_name, + "nepodporovaný formát", + )) + } + }; + + if video && !cfg.files.videos { + return Err(NekrochanError::FileError( + original_name, + "videa nejsou podporovaná", + )); + } + + let size = temp_file.size; + + if size / 1_000_000 > cfg.files.max_size_mb { + return Err(NekrochanError::FileError( + original_name, + "soubor je příliš velký", + )); + } + + let timestamp = Utc::now().timestamp_micros(); + let format = format.to_owned(); + + let (thumb_format, thumb_name) = if thumb { + let format = if video { "png".into() } else { format.clone() }; + + (Some(format.clone()), Some(format!("{timestamp}.{format}"))) + } else { + (None, None) + }; + + let path = temp_file.file.path().to_string_lossy().to_string(); + + let (width, height) = if video { + process_video(cfg, original_name.clone(), path.clone(), thumb_name).await? + } else { + process_image(cfg, original_name.clone(), path.clone(), thumb_name).await? + }; + + copy(path, format!("./uploads/{timestamp}.{format}")).await?; + + let file = File { + original_name, + format, + thumb_format, + spoiler, + width, + height, + timestamp, + size, + }; + + Ok(file) + } + + pub async fn delete(&self) { + remove_file(format!("./uploads/{}.{}", self.timestamp, self.format)) + .await + .ok(); + + if let Some(thumb_format) = &self.thumb_format { + remove_file(format!( + "./uploads/thumb/{}.{}", + self.timestamp, thumb_format + )) + .await + .ok(); + } + } + + pub fn file_url(&self) -> String { + format!("/uploads/{}.{}", self.timestamp, self.format) + } + + pub fn thumb_url(&self) -> String { + if self.spoiler { + "/static/spoiler.png".into() + } else if let Some(thumb_format) = &self.thumb_format { + format!("/uploads/thumb/{}.{}", self.timestamp, thumb_format) + } else { + self.file_url() + } + } +} + +async fn process_image( + cfg: &Cfg, + original_name: String, + path: String, + thumb_name: Option, +) -> Result<(u32, u32), NekrochanError> { + let path_ = path.clone(); + + let identify_out = spawn_blocking(move || { + Command::new("identify") + .args(["-format", "%wx%h", &format!("{path_}[0]")]) + .output() + }) + .await??; + + let invalid_dimensions = "imagemagick vrátil neplatné rozměry"; + let out_string = String::from_utf8_lossy(&identify_out.stdout); + + let (width, height) = out_string + .trim() + .split_once('x') + .ok_or(NekrochanError::FileError( + original_name.clone(), + invalid_dimensions, + ))?; + + let (width, height) = ( + width + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + height + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + ); + + if width > cfg.files.max_width || height > cfg.files.max_height { + return Err(NekrochanError::FileError( + original_name, + "rozměry obrázku jsou příliš velké", + )); + } + + let Some(thumb_name) = thumb_name else { + return Ok((width, height)); + }; + + let thumb_size = cfg.files.thumb_size; + + let output = spawn_blocking(move || { + Command::new("convert") + .arg(path) + .arg("-coalesce") + .arg("-thumbnail") + .arg(&format!("{thumb_size}x{thumb_size}>")) + .arg(&format!("./uploads/thumb/{thumb_name}")) + .output() + }) + .await??; + + if !output.status.success() { + println!("{}", String::from_utf8_lossy(&output.stderr)); + + return Err(NekrochanError::FileError( + original_name, + "nepodařilo se vytvořit náhled obrázku", + )); + } + + Ok((width, height)) +} + +async fn process_video( + cfg: &Cfg, + original_name: String, + path: String, + thumb_name: Option, +) -> Result<(u32, u32), NekrochanError> { + let path_ = path.clone(); + + let ffprobe_out = spawn_blocking(move || { + Command::new("ffprobe") + .args([ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + "-of", + "csv=s=x:p=0", + &path_, + ]) + .output() + }) + .await??; + + if !ffprobe_out.status.success() { + return Err(NekrochanError::FileError( + original_name, + "nepodařilo se získat rozměry videa", + )); + } + + let invalid_dimensions = "ffmpeg vrátil neplatné rozměry"; + let out_string = String::from_utf8_lossy(&ffprobe_out.stdout); + + let (width, height) = out_string + .trim() + .split_once('x') + .ok_or(NekrochanError::FileError( + original_name.clone(), + invalid_dimensions, + ))?; + + let (width, height) = ( + width + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + height + .parse() + .map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?, + ); + + if width > cfg.files.max_width || height > cfg.files.max_height { + return Err(NekrochanError::FileError( + original_name, + "rozměry videa jsou příliš velké", + )); + } + + let Some(thumb_name) = thumb_name else { + return Ok((width, height)); + }; + + let thumb_size = cfg.files.thumb_size; + + let output = spawn_blocking(move || { + Command::new("ffmpeg") + .args([ + "-i", + &path, + "-ss", + "00:00:00.50", + "-vframes", + "1", + "-vf", + &format!( + "scale={}", + if width > height { + format!("{thumb_size}:-2") + } else { + format!("-2:{thumb_size}") + } + ), + &format!("./uploads/thumb/{thumb_name}"), + ]) + .output() + }) + .await??; + + if !output.status.success() { + return Err(NekrochanError::FileError( + original_name, + "nepodařilo se vytvořit náhled videa", + )); + } + + Ok((width, height)) +} diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 0000000..3ba9a5c --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,87 @@ +use chrono::{DateTime, Locale, TimeZone, Utc}; +use chrono_tz::Europe::Prague; +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use std::{collections::HashSet, fmt::Display}; + +use crate::markup::SPOILER_REGEX; + +lazy_static! { + static ref MARKUP_QUOTE_REGEX: Regex = + Regex::new(r#">>(\d+)<\/a>"#).unwrap(); +} + +pub fn czech_datetime(utc: &DateTime) -> askama::Result { + let time = Prague.from_utc_datetime(&utc.naive_utc()); + + let time = time + .format_localized("%d.%m.%Y (%a) %H:%M:%S", Locale::cs_CZ) + .to_string(); + + Ok(time) +} + +pub fn czech_plural(plurals: &str, count: impl Display) -> askama::Result { + let plurals = plurals.split('|').collect::>(); + let count = count.to_string().parse::().unwrap(); + + let one = plurals[0]; + let few = plurals[1]; + let other = plurals[2]; + + if count == 1 { + Ok(one.into()) + } else if count < 5 && count != 0 { + Ok(few.into()) + } else { + Ok(other.into()) + } +} + +pub fn inline_post(input: impl Display) -> askama::Result { + let input = input.to_string(); + + if input.is_empty() { + return Ok("(bez obsahu)".into()); + } + + let collapsed = input.split_whitespace().collect::>().join(" "); + let spoilered = SPOILER_REGEX + .replace_all(&collapsed, "(spoiler)") + .to_string(); + + let truncated = askama::filters::truncate(spoilered, 64)?; + + Ok(truncated) +} + +pub fn get_page(input: &usize, page_size: &i64) -> askama::Result { + let page = crate::paginate(*page_size, *input as i64); + + Ok(page) +} + +pub fn add_yous( + input: impl Display, + board: &String, + yous: &HashSet, +) -> askama::Result { + let input = input.to_string(); + + let output = MARKUP_QUOTE_REGEX.replace_all(&input, |captures: &Captures| { + let quote = &captures[0]; + let id = &captures[1]; + + format!( + "{}{}", + quote, + if yous.contains(&format!("{board}/{id}")) { + " (Ty)" + } else { + "" + } + ) + }); + + Ok(output.to_string()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..8d45eb7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,61 @@ +use askama::Template; +use db::models::{Board, Post}; +use error::NekrochanError; +use web::tcx::TemplateCtx; + +const GENERIC_PAGE_SIZE: i64 = 10; + +pub mod auth; +pub mod cfg; +pub mod ctx; +pub mod db; +pub mod error; +pub mod files; +pub mod filters; +pub mod live_hub; +pub mod live_session; +pub mod markup; +pub mod perms; +pub mod qsform; +pub mod schedule; +pub mod trip; +pub mod web; + +pub fn paginate(page_size: i64, count: i64) -> i64 { + let pages = count / page_size + (count % page_size).signum(); + + if pages == 0 { + 1 + } else { + pages + } +} + +pub fn check_page( + page: i64, + pages: i64, + page_limit: impl Into>, +) -> Result<(), NekrochanError> { + if page <= 0 || (page > pages && page != 1) { + return Err(NekrochanError::InvalidPageError); + } + + if let Some(page_limit) = page_limit.into() { + if page > page_limit { + return Err(NekrochanError::InvalidPageError); + } + } + + Ok(()) +} + +#[derive(Template)] +#[template( + ext = "html", + source = "{% import \"./macros/post.html\" as post %}{% call post::post(board, post, post.thread.is_some()) %}" +)] +pub struct PostTemplate<'a> { + tcx: &'a TemplateCtx, + board: &'a Board, + post: &'a Post, +} diff --git a/src/live_hub.rs b/src/live_hub.rs new file mode 100644 index 0000000..a080cee --- /dev/null +++ b/src/live_hub.rs @@ -0,0 +1,272 @@ +use actix::{Actor, Context, Handler, Message, Recipient}; +use askama::Template; +use redis::Connection; +use serde_json::json; +use std::collections::HashMap; +use uuid::Uuid; + +use crate::{ + db::models::{Board, Post}, + web::tcx::TemplateCtx, + PostTemplate, +}; + +#[derive(Message)] +#[rtype(result = "()")] +pub enum SessionMessage { + Data(String), + Stop, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct ConnectMessage { + pub uuid: Uuid, + pub thread: (String, i64), + pub tcx: TemplateCtx, + pub recv: Recipient, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct DisconnectMessage { + pub uuid: Uuid, + pub thread: (String, i64), +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct PostCreatedMessage { + pub post: Post, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct TargetedPostCreatedMessage { + pub uuid: Uuid, + pub post: Post, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct PostUpdatedMessage { + pub post: Post, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct PostRemovedMessage { + pub post: Post, +} + +pub struct LiveHub { + pub cache: Connection, + pub recv_by_uuid: HashMap)>, + pub recv_by_thread: HashMap<(String, i64), Vec>, +} + +impl LiveHub { + pub fn new(cache: Connection) -> Self { + Self { + cache, + recv_by_uuid: HashMap::new(), + recv_by_thread: HashMap::new(), + } + } +} + +impl Actor for LiveHub { + type Context = Context; +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: ConnectMessage, _: &mut Self::Context) -> Self::Result { + self.recv_by_uuid.insert(msg.uuid, (msg.tcx, msg.recv)); + + match self.recv_by_thread.get_mut(&msg.thread) { + Some(vec) => { + vec.push(msg.uuid); + } + None => { + self.recv_by_thread.insert(msg.thread, vec![msg.uuid]); + } + } + } +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: DisconnectMessage, _: &mut Self::Context) -> Self::Result { + self.recv_by_uuid.remove(&msg.uuid); + + let recv_by_thread = match self.recv_by_thread.get_mut(&msg.thread) { + Some(recv_by_thread) => recv_by_thread, + None => return, + }; + + *recv_by_thread = recv_by_thread + .iter() + .filter(|uuid| **uuid != msg.uuid) + .map(Uuid::clone) + .collect(); + + if recv_by_thread.is_empty() { + self.recv_by_thread.remove(&msg.thread); + } + } +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: PostCreatedMessage, _: &mut Self::Context) -> Self::Result { + let post = msg.post; + + let uuids = self + .recv_by_thread + .get(&(post.board.clone(), post.thread.unwrap_or(post.id))); + + let uuids = match uuids { + Some(uuids) => uuids, + None => return, + }; + + let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else { + return; + }; + + for uuid in uuids { + let Some((tcx, recv)) = self.recv_by_uuid.get_mut(uuid) else { + continue; + }; + + tcx.update_yous(&mut self.cache).ok(); + + let tcx = &tcx; + let board = &board; + let post = &post; + let id = post.id; + + let html = PostTemplate { tcx, board, post } + .render() + .unwrap_or_default(); + + recv.do_send(SessionMessage::Data( + json!({ "type": "created", "id": id, "html": html }).to_string(), + )); + } + } +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: TargetedPostCreatedMessage, _: &mut Self::Context) -> Self::Result { + let post = msg.post; + + let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else { + return; + }; + + let Some((tcx, recv)) = self.recv_by_uuid.get(&msg.uuid) else { + return; + }; + + let id = post.id; + let tcx = &tcx; + let board = &board; + let post = &post; + + let html = PostTemplate { tcx, board, post } + .render() + .unwrap_or_default(); + + recv.do_send(SessionMessage::Data( + json!({ "type": "created", "id": id, "html": html }).to_string(), + )); + } +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: PostUpdatedMessage, _: &mut Self::Context) -> Self::Result { + let post = msg.post; + + let uuids = self + .recv_by_thread + .get(&(post.board.clone(), post.thread.unwrap_or(post.id))); + + let uuids = match uuids { + Some(uuids) => uuids, + None => return, + }; + + let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else { + return; + }; + + for uuid in uuids { + let Some((tcx, recv)) = self.recv_by_uuid.get_mut(uuid) else { + continue; + }; + + tcx.update_yous(&mut self.cache).ok(); + + let id = post.id; + let tcx = &tcx; + let board = &board; + let post = &post; + + let html = PostTemplate { tcx, post, board } + .render() + .unwrap_or_default(); + + recv.do_send(SessionMessage::Data( + json!({ "type": "updated", "id": id, "html": html }).to_string(), + )); + } + } +} + +impl Handler for LiveHub { + type Result = (); + + fn handle(&mut self, msg: PostRemovedMessage, _: &mut Self::Context) -> Self::Result { + let post = msg.post; + + let uuids = self + .recv_by_thread + .get(&(post.board.clone(), post.thread.unwrap_or(post.id))); + + let uuids = match uuids { + Some(uuids) => uuids, + None => return, + }; + + if post.thread.is_none() { + for uuid in uuids { + let Some((_, recv)) = self.recv_by_uuid.get(uuid) else { + continue; + }; + + recv.do_send(SessionMessage::Stop); + } + + return; + } + + for uuid in uuids { + let Some((_, recv)) = self.recv_by_uuid.get(uuid) else { + continue; + }; + + recv.do_send(SessionMessage::Data( + json!({ "type": "removed", "id": post.id }).to_string(), + )); + } + } +} diff --git a/src/live_session.rs b/src/live_session.rs new file mode 100644 index 0000000..c4d79ee --- /dev/null +++ b/src/live_session.rs @@ -0,0 +1,73 @@ +use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler}; +use actix_web_actors::ws::{Message as WsMessage, ProtocolError, WebsocketContext}; +use serde_json::json; +use uuid::Uuid; + +use crate::{ + live_hub::{ConnectMessage, DisconnectMessage, LiveHub, SessionMessage}, + web::tcx::TemplateCtx, +}; + +pub struct LiveSession { + pub uuid: Uuid, + pub thread: (String, i64), + pub tcx: TemplateCtx, + pub hub: Addr, +} + +impl Actor for LiveSession { + type Context = WebsocketContext; +} + +impl Handler for LiveSession { + type Result = (); + + fn handle(&mut self, msg: SessionMessage, ctx: &mut Self::Context) -> Self::Result { + match msg { + SessionMessage::Data(data) => ctx.text(data), + SessionMessage::Stop => { + ctx.text(json!({ "type": "thread_removed" }).to_string()); + self.finished(ctx) + } + }; + } +} + +impl StreamHandler> for LiveSession { + fn started(&mut self, ctx: &mut Self::Context) { + let uuid = self.uuid; + let thread = self.thread.clone(); + let tcx = self.tcx.clone(); + let recv = ctx.address().recipient(); + + self.hub.do_send(ConnectMessage { + uuid, + thread, + tcx, + recv, + }); + } + + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(WsMessage::Text(text)) => { + if text == "{\"type\":\"ping\"}" { + ctx.text("{\"type\":\"pong\"}"); + } + } + Ok(WsMessage::Ping(data)) => ctx.pong(&data), + Ok(WsMessage::Close(_)) => self.finished(ctx), + _ => (), + } + } + + fn finished(&mut self, ctx: &mut Self::Context) { + self.hub.do_send(DisconnectMessage { + uuid: self.uuid, + thread: self.thread.clone(), + }); + + ctx.close(None); + ctx.stop(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..0d03e90 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,188 @@ +use actix_files::{Files, NamedFile}; +use actix_web::{ + body::MessageBody, + dev::ServiceResponse, + get, + http::header::{HeaderValue, CACHE_CONTROL, PRAGMA}, + middleware::{ErrorHandlerResponse, ErrorHandlers}, + web::Data, + App, HttpRequest, HttpResponse, HttpServer, ResponseError, +}; +use anyhow::Error; +use askama::Template; +use log::{error, info}; +use nekrochan::{ + cfg::Cfg, + ctx::Ctx, + db::{cache::init_cache, models::Banner}, + error::NekrochanError, + schedule::s_cleanup_files, + web::{self, template_response}, +}; +use sqlx::migrate; +use std::{env::var, time::Duration}; +use tokio::time::sleep; + +#[actix_web::main] +async fn main() { + dotenv::dotenv().ok(); + env_logger::init(); + + if let Err(err) = run().await { + error!("{err:?}"); + } +} + +async fn run() -> Result<(), Error> { + let cfg_path = var("NEKROCHAN_CONFIG").unwrap_or_else(|_| "Nekrochan.toml".into()); + + let cfg = Cfg::load(&cfg_path).await?; + let ctx = Ctx::new(cfg).await?; + + migrate!().run(ctx.db()).await?; + init_cache(&ctx).await?; + + let ctx_ = ctx.clone(); + + tokio::spawn(async move { + loop { + match s_cleanup_files(&ctx_).await { + Ok(()) => info!("Routine file cleanup successful."), + Err(err) => error!("Routine file cleanup failed: {err:?}"), + }; + + sleep(Duration::from_secs(ctx_.cfg.files.cleanup_interval)).await; + } + }); + + let bind_addr = ctx.bind_addr(); + + HttpServer::new(move || { + App::new() + .app_data(Data::new(ctx.clone())) + .service(web::board::board) + .service(web::board_catalog::board_catalog) + .service(web::index::index) + .service(web::captcha::captcha) + .service(web::edit_posts::edit_posts) + .service(web::ip_posts::ip_posts) + .service(web::live::live) + .service(web::login::login_get) + .service(web::login::login_post) + .service(web::logout::logout) + .service(web::news::news) + .service(web::overboard::overboard) + .service(web::overboard_catalog::overboard_catalog) + .service(web::page::page) + .service(web::search::search) + .service(web::thread::thread) + .service(web::thread_json::thread_json) + .service(web::actions::appeal_ban::appeal_ban) + .service(web::actions::create_post::create_post) + .service(web::actions::edit_posts::edit_posts) + .service(web::actions::report_posts::report_posts) + .service(web::actions::staff_post_actions::staff_post_actions) + .service(web::actions::user_post_actions::user_post_actions) + .service(web::staff::account::account) + .service(web::staff::accounts::accounts) + .service(web::staff::bans::bans) + .service(web::staff::banners::banners) + .service(web::staff::board_config::board_config) + .service(web::staff::boards::boards) + .service(web::staff::edit_news::edit_news) + .service(web::staff::news::news) + .service(web::staff::permissions::permissions) + .service(web::staff::reports::reports) + .service(web::staff::actions::add_banners::add_banners) + .service(web::staff::actions::change_password::change_password) + .service(web::staff::actions::create_account::create_account) + .service(web::staff::actions::create_board::create_board) + .service(web::staff::actions::create_news::create_news) + .service(web::staff::actions::delete_account::delete_account) + .service(web::staff::actions::edit_news::edit_news) + .service(web::staff::actions::remove_accounts::remove_accounts) + .service(web::staff::actions::remove_banners::remove_banners) + .service(web::staff::actions::remove_bans::remove_bans) + .service(web::staff::actions::remove_boards::remove_boards) + .service(web::staff::actions::remove_news::remove_news) + .service(web::staff::actions::transfer_ownership::transfer_ownership) + .service(web::staff::actions::update_board_config::update_board_config) + .service(web::staff::actions::update_boards::update_boards) + .service(web::staff::actions::update_permissions::update_permissions) + .service(favicon) + .service(random_banner) + .service(Files::new("/static", "./static")) + .service(Files::new("/uploads", "./uploads").disable_content_disposition()) + .wrap(ErrorHandlers::new().default_handler(error_handler)) + }) + .bind(bind_addr)? + .run() + .await?; + + Ok(()) +} + +#[get("/favicon.ico")] +async fn favicon() -> Result { + let favicon = NamedFile::open("./static/favicon.ico")?; + + Ok(favicon) +} + +#[get("/random-banner")] +async fn random_banner(ctx: Data, req: HttpRequest) -> Result { + let file = if let Some(banner) = Banner::read_random(&ctx).await? { + let timestamp = banner.banner.timestamp; + let format = &banner.banner.format; + + NamedFile::open(format!("./uploads/{timestamp}.{format}"))? + } else { + NamedFile::open("./static/default-banner.png")? + }; + + let mut res = file.into_response(&req); + + res.headers_mut().append( + CACHE_CONTROL, + HeaderValue::from_static("no-cache, no-store, must-revalidate"), + ); + + res.headers_mut() + .append(PRAGMA, HeaderValue::from_static("no-cache")); + + Ok(res) +} + +#[derive(Template)] +#[template(path = "error.html")] +struct ErrorTempalate { + error_code: u16, + error_message: String, +} + +fn error_handler(res: ServiceResponse) -> actix_web::Result> +where + B: MessageBody, + ::Error: ResponseError + 'static, +{ + let (req, res) = res.into_parts(); + let status = res.status(); + + let error_code = status.as_u16(); + let error_message = match res.into_body().try_into_bytes().ok() { + Some(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(), + None => String::default(), + }; + + let template = ErrorTempalate { + error_code, + error_message, + }; + + let mut res = template_response(&template)?; + *(res.status_mut()) = status; + + let res = ServiceResponse::new(req, res).map_into_right_body(); + + Ok(ErrorHandlerResponse::Response(res)) +} diff --git a/src/markup.rs b/src/markup.rs new file mode 100644 index 0000000..79df423 --- /dev/null +++ b/src/markup.rs @@ -0,0 +1,229 @@ +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use sqlx::query_as; +use std::collections::HashMap; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + perms::PermissionWrapper, + trip::{secure_tripcode, tripcode}, +}; + +lazy_static! { + pub static ref NAME_REGEX: Regex = + Regex::new(r"^([^#].*?)?(?:(##([^ ].*?)|#([^#].*?)))?(##( .*?)?)?$").unwrap(); + pub static ref QUOTE_REGEX: Regex = Regex::new(r">>(\d+)").unwrap(); + pub static ref GREENTEXT_REGEX: Regex = Regex::new(r"(?mR)^>(.*)$").unwrap(); + pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?mR)^<(.*)$").unwrap(); + pub static ref REDTEXT_REGEX: Regex = Regex::new(r"==(.+?)==").unwrap(); + pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"--(.+?)--").unwrap(); + pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"\%\%(.+?)\%\%").unwrap(); + pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"\(\(\((.+?)\)\)\)").unwrap(); + pub static ref SPOILER_REGEX: Regex = Regex::new(r"\|\|([\s\S]+?)\|\|").unwrap(); + pub static ref URL_REGEX: Regex = + Regex::new(r"https?\://[^\s<>\[\]{}|\\^]+").unwrap(); + pub static ref JANNYTEXT_REGEX: Regex = Regex::new(r"##(.+?)##").unwrap(); +} + +pub fn parse_name( + ctx: &Ctx, + perms: &PermissionWrapper, + anon_name: &str, + name: &str, +) -> Result<(String, Option, Option), NekrochanError> { + let Some(captures) = NAME_REGEX.captures(name) else { + return Ok((anon_name.to_owned(), None, None)); + }; + + let name = match captures.get(1) { + Some(name) => { + let name = name.as_str().to_owned(); + + if name.len() > 32 { + return Err(NekrochanError::PostNameFormatError); + } + + name + } + None => anon_name.to_owned(), + }; + + let tripcode = match captures.get(2) { + Some(_) => { + let strip = captures.get(3); + let itrip = captures.get(4); + + if let Some(strip) = strip { + let trip = secure_tripcode(strip.as_str(), &ctx.cfg.secrets.secure_trip); + + Some(format!("!!{trip}")) + } else if let Some(itrip) = itrip { + let trip = tripcode(itrip.as_str()); + + Some(format!("!{trip}")) + } else { + None + } + } + None => None, + }; + + if !(perms.owner() || perms.capcodes()) { + return Ok((name, tripcode, None)); + } + + let capcode = match captures.get(5) { + Some(_) => match captures.get(6) { + Some(capcode) => { + let capcode: String = capcode.as_str().trim().into(); + + if capcode.is_empty() || !(perms.owner() || perms.custom_capcodes()) { + Some(capcode_fallback(perms.owner())) + } else { + if capcode.len() > 32 { + return Err(NekrochanError::CapcodeFormatError); + } + + Some(capcode) + } + } + None => Some(capcode_fallback(perms.owner())), + }, + None => None, + }; + + Ok((name, tripcode, capcode)) +} + +fn capcode_fallback(owner: bool) -> String { + if owner { + "Admin".into() + } else { + "Uklízeč".into() + } +} + +pub async fn markup( + ctx: &Ctx, + perms: &PermissionWrapper, + board: Option, + op: Option, + text: &str, +) -> Result<(String, Vec), NekrochanError> { + let text = escape_html(text); + + let (text, quoted_posts) = if let Some(board) = board { + let quoted_posts = get_quoted_posts(ctx, &board, &text).await?; + + let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { + let id_raw = &captures[1]; + + let Ok(id) = id_raw.parse() else { + return format!(">>{id_raw}"); + }; + + let post = quoted_posts.get(&id); + + if let Some(post) = post { + format!( + ">>{}{}", + post.post_url(), + post.id, + if op == Some(post.id) { + " (OP)" + } else { + "" + } + ) + } else { + format!(">>{id}") + } + }); + + let quoted_posts = quoted_posts + .into_values() + .filter(|post| op == Some(post.thread.unwrap_or(post.id))) + .collect(); + + (text.to_string(), quoted_posts) + } else { + (text, Vec::new()) + }; + + let text = GREENTEXT_REGEX.replace_all(&text, ">$1"); + let text = ORANGETEXT_REGEX.replace_all(&text, "<$1"); + let text = REDTEXT_REGEX.replace_all(&text, "$1"); + let text = BLUETEXT_REGEX.replace_all(&text, "$1"); + let text = GLOWTEXT_REGEX.replace_all(&text, "$1"); + let text = SPOILER_REGEX.replace_all(&text, "$1"); + + let text = UH_OH_TEXT_REGEX.replace_all(&text, |captures: &Captures| { + format!( + "((( {} )))", + captures[1].trim() + ) + }); + + let text = URL_REGEX.replace_all(&text, |captures: &Captures| { + let url = &captures[0]; + + format!("{url}") + }); + + let text = if perms.owner() || perms.jannytext() { + JANNYTEXT_REGEX.replace_all(&text, "$1") + } else { + text + }; + + Ok((text.to_string(), quoted_posts)) +} + +fn escape_html(text: &str) -> String { + text.replace('&', "&") + .replace('\'', "'") + .replace('/', "/") + .replace('`', "`") + .replace('=', "=") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +async fn get_quoted_posts( + ctx: &Ctx, + board: &String, + text: &str, +) -> Result, NekrochanError> { + let mut quoted_ids: Vec = Vec::new(); + + for quote in QUOTE_REGEX.captures_iter(text) { + let id_raw = "e[1]; + let Ok(id) = id_raw.parse() else { continue }; + + quoted_ids.push(id); + } + + if quoted_ids.is_empty() { + return Ok(HashMap::new()); + } + + let in_list = quoted_ids + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(","); + + let quoted_posts = query_as(&format!( + "SELECT * FROM posts_{board} WHERE id IN ({in_list})" + )) + .fetch_all(ctx.db()) + .await? + .into_iter() + .map(|post: Post| (post.id, post)) + .collect::>(); + + Ok(quoted_posts) +} diff --git a/src/perms.rs b/src/perms.rs new file mode 100755 index 0000000..c10e8ec --- /dev/null +++ b/src/perms.rs @@ -0,0 +1,111 @@ +use enumflags2::{bitflags, BitFlags}; + +#[bitflags] +#[repr(u64)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Permissions { + EditPosts, + ManagePosts, + Capcodes, + CustomCapcodes, + StaffLog, + Reports, + Bans, + BoardBanners, + BoardConfig, + News, + Jannytext, + ViewIPs, + BypassBans, + BypassBoardLock, + BypassThreadLock, + BypassCaptcha, + BypassAntispam, +} + +#[derive(Debug, Clone)] +pub struct PermissionWrapper(BitFlags, bool); + +impl PermissionWrapper { + pub fn new(perms: u64, owner: bool) -> Self { + Self(BitFlags::from_bits_truncate(perms), owner) + } +} + +impl PermissionWrapper { + pub fn integer(&self) -> u64 { + self.0.bits() + } + + pub fn owner(&self) -> bool { + self.1 + } + + pub fn edit_posts(&self) -> bool { + self.0.contains(Permissions::EditPosts) + } + + pub fn manage_posts(&self) -> bool { + self.0.contains(Permissions::ManagePosts) + } + + pub fn capcodes(&self) -> bool { + self.0.contains(Permissions::Capcodes) + } + + pub fn custom_capcodes(&self) -> bool { + self.0.contains(Permissions::CustomCapcodes) + } + + pub fn staff_log(&self) -> bool { + self.0.contains(Permissions::StaffLog) + } + + pub fn reports(&self) -> bool { + self.0.contains(Permissions::Reports) + } + + pub fn bans(&self) -> bool { + self.0.contains(Permissions::Bans) + } + + pub fn banners(&self) -> bool { + self.0.contains(Permissions::BoardBanners) + } + + pub fn board_config(&self) -> bool { + self.0.contains(Permissions::BoardConfig) + } + + pub fn news(&self) -> bool { + self.0.contains(Permissions::News) + } + + pub fn jannytext(&self) -> bool { + self.0.contains(Permissions::Jannytext) + } + + pub fn view_ips(&self) -> bool { + self.0.contains(Permissions::ViewIPs) + } + + pub fn bypass_bans(&self) -> bool { + self.0.contains(Permissions::BypassBans) + } + + pub fn bypass_board_lock(&self) -> bool { + self.0.contains(Permissions::BypassBoardLock) + } + + pub fn bypass_thread_lock(&self) -> bool { + self.0.contains(Permissions::BypassThreadLock) + } + + pub fn bypass_captcha(&self) -> bool { + self.0.contains(Permissions::BypassCaptcha) + } + + pub fn bypass_antispam(&self) -> bool { + self.0.contains(Permissions::BypassAntispam) + } +} diff --git a/src/qsform.rs b/src/qsform.rs new file mode 100644 index 0000000..ccfaa53 --- /dev/null +++ b/src/qsform.rs @@ -0,0 +1,43 @@ +use actix_web::{dev::Payload, http::StatusCode, FromRequest, HttpRequest, ResponseError}; +use serde::Deserialize; +use serde_qs::Config; +use std::{future::Future, pin::Pin}; +use thiserror::Error; + +pub struct QsForm(pub T) +where + T: for<'de> Deserialize<'de>; + +#[derive(Debug, Error)] +pub enum QsFormError { + #[error("{}", .0)] + FutureError(#[from] actix_web::Error), + #[error("{}", .0)] + ParseError(#[from] serde_qs::Error), +} + +impl ResponseError for QsFormError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl FromRequest for QsForm +where + T: for<'de> Deserialize<'de>, +{ + type Error = QsFormError; + type Future = Pin, Self::Error>>>>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let data = String::from_request(req, payload); + + Box::pin(async move { + let data = data.await?; + let config = Config::new(10, false); + let form: T = config.deserialize_str(&data)?; + + Ok(QsForm(form)) + }) + } +} diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..ba94978 --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,67 @@ +use anyhow::Error; +use glob::glob; +use std::collections::HashSet; +use tokio::fs::remove_file; + +use crate::{ + ctx::Ctx, + db::models::{Banner, Board, Post}, +}; + +pub async fn s_cleanup_files(ctx: &Ctx) -> Result<(), Error> { + let mut keep = HashSet::new(); + let mut keep_thumbs = HashSet::new(); + + let banners = Banner::read_all(ctx).await?; + + for banner in banners { + keep.insert(format!( + "{}.{}", + banner.banner.timestamp, banner.banner.format + )); + } + + let boards = Board::read_all(ctx).await?; + + for board in boards { + let posts = Post::read_all(ctx, board.id.clone()).await?; + + for post in posts { + for file in post.files.0 { + keep.insert(format!("{}.{}", file.timestamp, file.format)); + + if let Some(thumb_format) = file.thumb_format { + keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format)); + } + } + } + } + + for file in glob("./uploads/*.*")? { + let file = file?; + let file_name = file.file_name(); + + if let Some(file_name) = file_name { + let check = file_name.to_string_lossy().to_string(); + + if !keep.contains(&check) { + remove_file(file).await?; + } + } + } + + for file in glob("./uploads/thumb/*.*")? { + let file = file?; + let file_name = file.file_name(); + + if let Some(file_name) = file_name { + let check = file_name.to_string_lossy().to_string(); + + if !keep_thumbs.contains(&check) { + remove_file(file).await?; + } + } + } + + Ok(()) +} diff --git a/src/trip.rs b/src/trip.rs new file mode 100755 index 0000000..0818739 --- /dev/null +++ b/src/trip.rs @@ -0,0 +1,54 @@ +use encoding::{all::WINDOWS_31J, EncoderTrap, Encoding}; +use pwhash::{ + bcrypt::{self, BcryptSetup}, + unix::crypt, +}; + +pub fn tripcode(password: &str) -> String { + let password = WINDOWS_31J.encode(password, EncoderTrap::Replace).unwrap(); + + let salt = [password.as_ref(), "H.".as_bytes()].concat(); + let salt = &salt[1..3]; + let salt = salt + .iter() + .map(|c| match c { + 46..=122 => *c, + _ => 46, + } as char) + .map(|c| match c { + ':' => 'A', + ';' => 'B', + '<' => 'C', + '=' => 'D', + '>' => 'E', + '?' => 'F', + '@' => 'G', + '[' => 'a', + '\\' => 'b', + ']' => 'c', + '^' => 'd', + '_' => 'e', + '`' => 'f', + _ => c, + }) + .collect::(); + + let trip = crypt(password, &salt).unwrap(); + + trip[3..].to_owned() +} + +pub fn secure_tripcode(password: &str, tripcode_secret: &str) -> String { + let trip = bcrypt::hash_with( + BcryptSetup { + salt: Some(tripcode_secret), + ..Default::default() + }, + password, + ) + .unwrap(); + + let trip = &trip[trip.len() - 10..]; + + trip.into() +} diff --git a/src/web/actions/appeal_ban.rs b/src/web/actions/appeal_ban.rs new file mode 100644 index 0000000..e6e02f9 --- /dev/null +++ b/src/web/actions/appeal_ban.rs @@ -0,0 +1,59 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use super::ActionTemplate; +use crate::{ + ctx::Ctx, + db::models::Ban, + error::NekrochanError, + qsform::QsForm, + web::{ + tcx::{ip_from_req, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct AppealBanForm { + pub id: i32, + pub appeal: String, +} + +#[post("/actions/appeal-ban")] +pub async fn appeal_ban( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let (ip, _) = ip_from_req(&req)?; + + let ban = Ban::read_by_id(&ctx, form.id) + .await? + .ok_or(NekrochanError::BanNotFound)?; + + if !ban.ip_range.contains(ip) { + return Err(NekrochanError::BanNotFound); + } + + if ban.appeal.is_some() { + return Err(NekrochanError::AlreadyAppealedError); + } + + if !ban.appealable { + return Err(NekrochanError::UnappealableError); + } + + let appeal: String = form.appeal.trim().into(); + + if appeal.is_empty() || appeal.len() > 1000 { + return Err(NekrochanError::BanAppealFormatError); + } + + ban.update_appeal(&ctx, appeal).await?; + + template_response(&ActionTemplate { + tcx, + response: "Ban byl úspěšně odvolán.".into(), + }) +} diff --git a/src/web/actions/create_post.rs b/src/web/actions/create_post.rs new file mode 100644 index 0000000..b138249 --- /dev/null +++ b/src/web/actions/create_post.rs @@ -0,0 +1,341 @@ +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +use actix_web::{ + cookie::Cookie, http::StatusCode, post, web::Data, HttpRequest, HttpResponse, + HttpResponseBuilder, +}; +use chrono::{Duration, Utc}; +use pwhash::bcrypt::hash; +use redis::AsyncCommands; +use sha256::digest; +use std::{collections::HashSet, net::IpAddr}; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board, File, Post}, + error::NekrochanError, + markup::{markup, parse_name}, + perms::PermissionWrapper, + web::{ + ban_response, + tcx::{account_from_auth_opt, ip_from_req}, + }, +}; + +#[derive(MultipartForm)] +pub struct PostForm { + pub board: Text, + pub thread: Option>, + #[multipart(rename = "post_name")] + pub name: Text, + pub email: Text, + pub content: Text, + #[multipart(rename = "files[]")] + pub files: Vec, + pub spoiler_files: Option>, + #[multipart(rename = "post_password")] + pub password: Text, + pub captcha_id: Option>, + pub captcha_solution: Option>, +} + +#[post("/actions/create-post")] +pub async fn create_post( + ctx: Data, + req: HttpRequest, + MultipartForm(form): MultipartForm, +) -> Result { + let perms = match account_from_auth_opt(&ctx, &req).await? { + Some(account) => account.perms(), + None => PermissionWrapper::new(0, false), + }; + + let (ip, country) = ip_from_req(&req)?; + + let board = form.board.0; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + if let Some(ban) = Ban::read(&ctx, board.id.clone(), ip).await? { + if !(perms.owner() || perms.bypass_bans()) { + return ban_response(&ctx, &req, ban).await; + } + } + + if board.config.0.locked && !(perms.owner() || perms.bypass_board_lock()) { + return Err(NekrochanError::BoardLockError(board.id.clone())); + } + + let mut bump = true; + let mut noko = ctx.cfg.site.noko; + + let thread = match form.thread { + Some(Text(thread)) => { + let thread = Post::read(&ctx, board.id.clone(), thread) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), thread))?; + + if thread.thread.is_some() { + return Err(NekrochanError::IsReplyError); + } + + if thread.locked && !(perms.owner() || perms.bypass_thread_lock()) { + return Err(NekrochanError::ThreadLockError); + } + + if thread.replies >= board.config.0.reply_limit { + return Err(NekrochanError::ReplyLimitError); + } + + if thread.bumps >= board.config.0.bump_limit { + bump = false; + } + + Some(thread) + } + None => None, + }; + + if !(perms.owner() || perms.bypass_captcha()) + && ((thread.is_none() && board.config.0.thread_captcha != "off") + || (thread.is_some() && board.config.0.reply_captcha != "off")) + { + let board = board.id.clone(); + + let id = form + .captcha_id + .ok_or(NekrochanError::RequiredCaptchaError)? + .0; + + if id.is_empty() { + return Err(NekrochanError::RequiredCaptchaError); + } + + let key = format!("captcha:{board}:{id}"); + + let solution = form + .captcha_solution + .ok_or(NekrochanError::RequiredCaptchaError)?; + + let actual_solution: Option = ctx.cache().get_del(key).await?; + let actual_solution = actual_solution.ok_or(NekrochanError::InvalidCaptchaError)?; + + if solution.trim() != actual_solution { + return Err(NekrochanError::IncorrectCaptchaError); + } + } + + let name_raw = form.name.trim(); + let (name, tripcode, capcode) = parse_name(&ctx, &perms, &board.config.0.anon_name, name_raw)?; + + let email_raw = form.email.trim(); + + let email = if email_raw.is_empty() { + None + } else { + if email_raw.len() > 256 { + return Err(NekrochanError::EmailFormatError); + } + + let email_lower = email_raw.to_lowercase(); + + if email_lower == "sage" { + bump = false; + } + + if !ctx.cfg.site.noko && email_lower == "noko" { + noko = true + } + + if ctx.cfg.site.noko { + if email_lower == "nonoko" { + noko = false; + } + + if email_lower == "nonokosage" { + noko = false; + bump = false; + } + } else { + if email_lower == "noko" { + noko = true; + } + + if email_lower == "nokosage" { + noko = true; + bump = false; + } + } + + Some(email_raw.into()) + }; + + let password_raw = form.password.trim(); + + if password_raw.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(password_raw)?; + + if form.files.len() > board.config.0.file_limit { + return Err(NekrochanError::FileLimitError(board.config.0.file_limit)); + } + + let mut files = Vec::new(); + + for file in form.files { + if file.size == 0 { + continue; + } + + let spoiler = form.spoiler_files.is_some(); + let file = File::new(&ctx.cfg, file, spoiler, true).await?; + + files.push(file); + } + + let thread_id = thread.as_ref().map(|t| t.id); + let content_nomarkup = form.content.0.trim().to_owned(); + + if board.config.antispam && !(perms.owner() || perms.bypass_antispam()) { + check_spam(&ctx, &board, ip, content_nomarkup.clone()).await?; + } + + if content_nomarkup.is_empty() && files.is_empty() { + return Err(NekrochanError::EmptyPostError); + } + + if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content) + || (thread.is_some() && board.config.0.require_reply_content) + { + return Err(NekrochanError::NoContentError); + } + + if content_nomarkup.len() > 10000 { + return Err(NekrochanError::ContentFormatError); + } + + let (content, quoted_posts) = markup( + &ctx, + &perms, + Some(board.id.clone()), + thread.as_ref().map(|t| t.id), + &content_nomarkup, + ) + .await?; + + let post = Post::create( + &ctx, + &board, + thread_id, + name, + tripcode, + capcode, + email, + content, + content_nomarkup, + files, + password, + country, + ip, + bump, + ) + .await?; + + for quoted_post in quoted_posts { + quoted_post.update_quotes(&ctx, post.id).await?; + } + + let ts = thread.as_ref().map_or_else( + || post.created.timestamp_micros(), + |thread| thread.created.timestamp_micros(), + ); + + let hash_input = format!("{}:{}:{}", ip, ts, ctx.cfg.secrets.user_id); + let user_hash = digest(hash_input); + let user_id = user_hash[..6].to_owned(); + + post.update_user_id(&ctx, user_id).await?; + + let mut res = HttpResponseBuilder::new(StatusCode::SEE_OTHER); + + let name_cookie = Cookie::build("name", name_raw).path("/").finish(); + let password_cookie = Cookie::build("password", password_raw).path("/").finish(); + let email_cookie = Cookie::build("email", email_raw).path("/").finish(); + + res.cookie(name_cookie); + res.cookie(password_cookie); + res.cookie(email_cookie); + + let res = if noko { + res.append_header(("Location", post.post_url().as_str())) + .finish() + } else { + res.append_header(("Location", format!("/boards/{}", post.board).as_str())) + .finish() + }; + + Ok(res) +} + +pub async fn check_spam( + ctx: &Ctx, + board: &Board, + ip: IpAddr, + content_nomarkup: String, +) -> Result<(), NekrochanError> { + let ip_key = format!("by_ip:{ip}"); + let content_key = format!("by_content:{}", digest(content_nomarkup)); + + let antispam_ip = (Utc::now() - Duration::seconds(board.config.antispam_ip)).timestamp_micros(); + let antispam_content = + (Utc::now() - Duration::seconds(board.config.antispam_content)).timestamp_micros(); + let antispam_both = + (Utc::now() - Duration::seconds(board.config.antispam_both)).timestamp_micros(); + + let ip_posts: HashSet = ctx + .cache() + .zrangebyscore(&ip_key, antispam_ip, "+inf") + .await?; + let content_posts: HashSet = ctx + .cache() + .zrangebyscore(&content_key, antispam_content, "+inf") + .await?; + + let ip_posts2: HashSet = ctx + .cache() + .zrangebyscore(&ip_key, antispam_both, "+inf") + .await?; + let content_posts2: HashSet = ctx + .cache() + .zrangebyscore(&content_key, antispam_both, "+inf") + .await?; + + let both_posts = ip_posts2.intersection(&content_posts2); + + if !ip_posts.is_empty() { + return Err(NekrochanError::FloodError); + } + + if !content_posts.is_empty() { + return Err(NekrochanError::FloodError); + } + + if both_posts.count() != 0 { + return Err(NekrochanError::FloodError); + } + + let last_thread: Option = ctx.cache().get(format!("last_thread:{ip}")).await?; + + if let Some(last_thread) = last_thread { + let since_last_thread = Utc::now().timestamp_micros() - last_thread; + let since_last_thread = Duration::microseconds(since_last_thread); + + if since_last_thread.num_seconds() < board.config.thread_cooldown { + return Err(NekrochanError::FloodError); + } + } + + Ok(()) +} diff --git a/src/web/actions/edit_posts.rs b/src/web/actions/edit_posts.rs new file mode 100644 index 0000000..22b0368 --- /dev/null +++ b/src/web/actions/edit_posts.rs @@ -0,0 +1,76 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use sqlx::query; +use std::{collections::HashMap, fmt::Write}; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + markup::markup, + qsform::QsForm, + web::{tcx::TemplateCtx, template_response}, +}; + +use super::{get_posts_from_ids, ActionTemplate}; + +#[post("/actions/edit-posts")] +pub async fn edit_posts( + ctx: Data, + req: HttpRequest, + QsForm(edits): QsForm>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.edit_posts()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let ids = edits.keys().map(|s| s.to_owned()).collect::>(); + + let posts = get_posts_from_ids(&ctx, &ids) + .await + .into_iter() + .map(|post| (format!("{}/{}", post.board, post.id), post)) + .collect::>(); + + let mut response = String::new(); + let mut posts_edited = 0; + + for (key, content_nomarkup) in edits { + let post = &posts[&key]; + let content_nomarkup = content_nomarkup.trim(); + let (content, quoted_posts) = markup( + &ctx, + &tcx.perms, + Some(post.board.clone()), + post.thread, + content_nomarkup, + ) + .await?; + + post.update_content(&ctx, content, content_nomarkup.into()) + .await?; + + query(&format!( + "UPDATE posts_{} SET quotes = array_remove(quotes, $1) WHERE $1 = ANY(quotes)", + post.board + )) + .bind(post.id) + .execute(ctx.db()) + .await?; + + for quoted_post in quoted_posts { + quoted_post.update_quotes(&ctx, post.id).await?; + } + + posts_edited += 1; + } + + if posts_edited != 0 { + writeln!(&mut response, "[Úspěch] Upraveny příspěvky: {posts_edited}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/actions/mod.rs b/src/web/actions/mod.rs new file mode 100644 index 0000000..8f9e0e8 --- /dev/null +++ b/src/web/actions/mod.rs @@ -0,0 +1,46 @@ +use askama::Template; +use sqlx::query_as; + +use super::tcx::TemplateCtx; +use crate::{ctx::Ctx, db::models::Post}; + +pub mod appeal_ban; +pub mod create_post; +pub mod edit_posts; +pub mod report_posts; +pub mod staff_post_actions; +pub mod user_post_actions; + +#[derive(Template)] +#[template(path = "action.html")] +pub struct ActionTemplate { + pub tcx: TemplateCtx, + pub response: String, +} + +pub async fn get_posts_from_ids(ctx: &Ctx, ids: &Vec) -> Vec { + let mut posts = Vec::new(); + + for id in ids { + if let Some((board, id)) = parse_id(id) { + if let Ok(Some(post)) = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2") + .bind(board) + .bind(id) + .fetch_optional(ctx.db()) + .await + { + posts.push(post); + } + } + } + + posts +} + +fn parse_id(id: &str) -> Option<(String, i64)> { + let (board, id) = id.split_once('/')?; + let board = board.to_owned(); + let id = id.parse().ok()?; + + Some((board, id)) +} diff --git a/src/web/actions/report_posts.rs b/src/web/actions/report_posts.rs new file mode 100644 index 0000000..b98347b --- /dev/null +++ b/src/web/actions/report_posts.rs @@ -0,0 +1,110 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; +use std::fmt::Write; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board}, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + ban_response, + tcx::{ip_from_req, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct ReportPostsForm { + #[serde(default)] + pub posts: Vec, + pub report_reason: String, +} + +#[post("/actions/report-posts")] +pub async fn report_posts( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let (reporter_ip, reporter_country) = ip_from_req(&req)?; + let bans = Ban::read_by_ip(&ctx, reporter_ip).await?; + + if let Some(ban) = bans.get(&None) { + if !(tcx.perms.owner() || tcx.perms.bypass_bans()) { + return ban_response(&ctx, &req, ban.clone()).await; + } + } + + let boards = Board::read_all_map(&ctx).await?; + let posts = get_posts_from_ids(&ctx, &form.posts).await; + + let mut response = String::new(); + let mut posts_reported = 0; + + let reason = form.report_reason.trim(); + + if reason.is_empty() || reason.len() > 200 { + return Err(NekrochanError::ReportFormatError); + } + + for post in &posts { + let board = &boards[&post.board]; + + if bans.contains_key(&Some(board.id.clone())) + && !(tcx.perms.owner() || tcx.perms.bypass_bans()) + { + writeln!(&mut response, "[Chyba] Jsi zabanován z /{}/.", board.id).ok(); + continue; + } + + if board.config.0.locked && !(tcx.perms.owner() || tcx.perms.bypass_board_lock()) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::BoardLockError(board.id.clone()) + ) + .ok(); + + continue; + } + + if post + .reports + .iter() + .any(|report| report.reporter_ip == reporter_ip) + { + writeln!( + &mut response, + "[Chyba] Příspěvek #{} jsi už nahlásil.", + post.id + ) + .ok(); + continue; + } + + post.create_report( + &ctx, + reason.to_owned(), + reporter_country.clone(), + reporter_ip, + ) + .await?; + + posts_reported += 1; + } + + if posts_reported != 0 { + writeln!( + &mut response, + "[Úspěch] Nahlášeny příspěvky: {posts_reported}" + ) + .ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/actions/staff_post_actions.rs b/src/web/actions/staff_post_actions.rs new file mode 100644 index 0000000..423684f --- /dev/null +++ b/src/web/actions/staff_post_actions.rs @@ -0,0 +1,369 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use chrono::{Duration, Utc}; +use ipnetwork::IpNetwork; +use redis::AsyncCommands; +use serde::Deserialize; +use std::{collections::HashSet, fmt::Write, net::IpAddr}; + +use crate::{ + ctx::Ctx, + db::models::{Account, Ban}, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct StaffPostActionsForm { + #[serde(default)] + pub posts: Vec, + #[serde(rename = "staff_remove_posts")] + pub remove_posts: Option, + #[serde(rename = "staff_remove_files")] + pub remove_files: Option, + #[serde(rename = "staff_toggle_spoiler")] + pub toggle_spoiler: Option, + pub remove_by_ip_board: Option, + pub remove_by_ip_global: Option, + pub toggle_sticky: Option, + pub toggle_lock: Option, + pub remove_reports: Option, + pub ban_user: Option, + pub ban_reporters: Option, + pub global_ban: Option, + pub unappealable_ban: Option, + pub ban_reason: Option, + pub ban_duration: Option, + pub ban_range: Option, + pub troll_user: Option, +} + +#[post("/actions/staff-post-actions")] +pub async fn staff_post_actions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + let posts = get_posts_from_ids(&ctx, &form.posts).await; + + let mut response = String::new(); + + let mut posts_removed = 0; + let mut files_removed = 0; + let mut spoilers_toggled = 0; + let mut stickies_toggled = 0; + let mut locks_toggled = 0; + let mut reports_removed = 0; + let mut bans_issued = 0; + let mut users_trolled = 0; + + for post in &posts { + if (form.remove_posts.is_some() + || form.remove_files.is_some() + || form.toggle_spoiler.is_some() + || form.toggle_sticky.is_some() + || form.toggle_lock.is_some()) + && !(account.perms().owner() || account.perms().manage_posts()) + { + writeln!( + &mut response, + "[Chyba] Nemáš oprávnění spravovat příspěvky." + ) + .ok(); + + continue; + } + + if form.remove_posts.is_some() { + post.delete(&ctx).await?; + posts_removed += 1; + } + + if form.remove_files.is_some() { + post.delete_files(&ctx).await?; + files_removed += post.files.0.len(); + } + + if form.toggle_spoiler.is_some() { + post.update_spoiler(&ctx).await?; + spoilers_toggled += post.files.0.len(); + } + + if form.remove_by_ip_board.is_some() { + let key = format!("by_ip:{}", post.ip); + let ip_posts: Vec = ctx.cache().zrange(key, 0, -1).await?; + let board_ip_posts = ip_posts + .into_iter() + .filter(|p| p.starts_with(&format!("{}/", post.board))) + .collect::>(); + + for post in get_posts_from_ids(&ctx, &board_ip_posts).await { + post.delete(&ctx).await?; + posts_removed += 1; + } + } + + if form.remove_by_ip_global.is_some() { + let key = format!("by_ip:{}", post.ip); + let ip_posts: Vec = ctx.cache().zrange(key, 0, -1).await?; + + for post in get_posts_from_ids(&ctx, &ip_posts).await { + post.delete(&ctx).await?; + posts_removed += 1; + } + } + + if form.toggle_sticky.is_some() { + post.update_sticky(&ctx).await?; + stickies_toggled += 1; + } + + if form.toggle_lock.is_some() { + if post.thread.is_some() { + writeln!(&mut response, "[Chyba] Odpověď nelze uzamknout.").ok(); + } else { + post.update_lock(&ctx).await?; + locks_toggled += 1; + } + } + } + + for post in &posts { + if form.remove_reports.is_some() { + if !(tcx.perms.owner() || tcx.perms.reports()) { + writeln!(&mut response, "[Chyba] Nemáš přístup k hlášením.").ok(); + continue; + } + + post.delete_reports(&ctx).await?; + reports_removed += post.reports.0.len(); + } + } + + let mut already_banned = HashSet::new(); + + for post in &posts { + if let ((Some(_), _) | (_, Some(_)), reason, duration, Some(range)) = ( + (form.ban_user.clone(), form.ban_reporters.clone()), + form.ban_reason.clone().unwrap_or_default(), + form.ban_duration.unwrap_or_default(), + form.ban_range.clone(), + ) { + if !(account.perms().owner() || account.perms().bans()) { + writeln!(&mut response, "[Chyba] Nemáš oprávnění vydat ban.").ok(); + continue; + } + + let mut ips_to_ban = HashSet::new(); + + if form.ban_user.is_some() && !already_banned.contains(&post.ip) { + ips_to_ban.insert(post.ip); + } + + if form.ban_reporters.is_some() { + if !(tcx.perms.owner() || tcx.perms.reports()) { + writeln!(&mut response, "[Chyba] Nemáš přístup k hlášením.").ok(); + continue; + } + + ips_to_ban.extend( + post.reports + .0 + .iter() + .map(|r| r.reporter_ip) + .filter(|ip| !already_banned.contains(ip)), + ) + } + + if ips_to_ban.is_empty() { + continue; + } + + for ip in &ips_to_ban { + ban_ip( + &ctx, + &account, + &form, + *ip, + post.board.clone(), + reason.clone(), + duration, + &range, + ) + .await?; + } + + if form.ban_user.is_some() { + let content_nomarkup = format!( + "{}\n\n##(UŽIVATEL BYL ZA TENTO PŘÍSPĚVEK ZABANOVÁN)##", + post.content_nomarkup + ); + + let content = format!( + "{}\n\n(UŽIVATEL BYL ZA TENTO PŘÍSPĚVEK ZABANOVÁN)", + post.content + ); + + post.update_content(&ctx, content, content_nomarkup).await?; + } + + bans_issued += ips_to_ban.len(); + already_banned.extend(ips_to_ban); + } + } + + for post in &posts { + if form.troll_user.is_some() { + if !(tcx.perms.owner() || tcx.perms.edit_posts()) { + writeln!( + &mut response, + "[Chyba] Nemáš oprávnění upravovat příspěvky." + ) + .ok(); + continue; + } + + if !(tcx.perms.owner() || tcx.perms.view_ips()) { + writeln!( + &mut response, + "[Chyba] Nemáš oprávnění zobrazovat IP adresy." + ) + .ok(); + continue; + } + + let content_nomarkup = format!("{}\n\n##({})##", post.content_nomarkup, post.ip); + + let content = format!( + "{}\n\n({})", + post.content, post.ip + ); + + post.update_content(&ctx, content, content_nomarkup).await?; + users_trolled += 1; + } + } + + if posts_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny příspěvky: {posts_removed}" + ) + .ok(); + } + + if files_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny soubory: {files_removed}" + ) + .ok(); + } + + if spoilers_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Přepnuty spoilery: {spoilers_toggled}" + ) + .ok(); + } + + if stickies_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Připnuty/odepnuty příspěvky: {stickies_toggled}" + ) + .ok(); + } + + if locks_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Zamčena/odemčena vlákna: {locks_toggled}" + ) + .ok(); + } + + if reports_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněna hlášení: {reports_removed}" + ) + .ok(); + } + + if users_trolled != 0 { + writeln!( + &mut response, + "[Úspěch] Vytroleni uživatelé: {users_trolled}" + ) + .ok(); + } + + if bans_issued != 0 { + writeln!(&mut response, "[Úspěch] Uděleny bany: {bans_issued}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} + +#[allow(clippy::too_many_arguments)] +async fn ban_ip( + ctx: &Ctx, + account: &Account, + form: &StaffPostActionsForm, + ip: IpAddr, + board: String, + reason: String, + duration: u64, + range: &str, +) -> Result<(), NekrochanError> { + let account = account.username.clone(); + + let board = if form.global_ban.is_none() { + Some(board) + } else { + None + }; + + let prefix = if ip.is_ipv4() { + match range { + "lan" => 24, + "isp" => 16, + _ => 32, + } + } else { + match range { + "lan" => 48, + "isp" => 24, + _ => 128, + } + }; + + let ip_range = IpNetwork::new(ip, prefix)?; + let reason: String = reason.trim().into(); + + if reason.is_empty() || reason.len() > 200 { + return Err(NekrochanError::BanReasonFormatError); + } + + let appealable = form.unappealable_ban.is_none(); + + let expires = if duration == 0 { + None + } else { + Some(Utc::now() + Duration::days(duration as i64)) + }; + + Ban::create(ctx, account, board, ip_range, reason, appealable, expires).await?; + + Ok(()) +} diff --git a/src/web/actions/user_post_actions.rs b/src/web/actions/user_post_actions.rs new file mode 100644 index 0000000..6c9f580 --- /dev/null +++ b/src/web/actions/user_post_actions.rs @@ -0,0 +1,135 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::verify; +use serde::Deserialize; +use std::fmt::Write; + +use crate::{ + ctx::Ctx, + db::models::{Ban, Board}, + error::NekrochanError, + qsform::QsForm, + web::{ + actions::{get_posts_from_ids, ActionTemplate}, + ban_response, + tcx::{ip_from_req, TemplateCtx}, + template_response, + }, +}; + +#[derive(Deserialize)] +pub struct UserPostActionsForm { + #[serde(default)] + pub posts: Vec, + pub remove_posts: Option, + pub remove_files: Option, + pub toggle_spoiler: Option, + #[serde(rename = "post_password")] + pub password: String, +} + +#[post("/actions/user-post-actions")] +pub async fn user_post_actions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let (ip, _) = ip_from_req(&req)?; + let bans = Ban::read_by_ip(&ctx, ip).await?; + + if let Some(ban) = bans.get(&None) { + if !(tcx.perms.owner() || tcx.perms.bypass_bans()) { + return ban_response(&ctx, &req, ban.clone()).await; + } + } + + let posts = get_posts_from_ids(&ctx, &form.posts).await; + let boards = Board::read_all_map(&ctx).await?; + + let mut response = String::new(); + + let mut posts_removed = 0; + let mut files_removed = 0; + let mut spoilers_toggled = 0; + + for post in &posts { + let board = &boards[&post.board]; + + if bans.contains_key(&Some(board.id.clone())) + && !(tcx.perms.owner() || tcx.perms.bypass_bans()) + { + writeln!(&mut response, "[Chyba] Jsi zabanován z /{}/.", board.id).ok(); + continue; + } + + if board.config.0.locked && !(tcx.perms.owner() || tcx.perms.bypass_board_lock()) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::BoardLockError(board.id.clone()) + ) + .ok(); + + continue; + } + + if !verify(&form.password, &post.password) { + writeln!( + &mut response, + "[Chyba] {}", + NekrochanError::IncorrectPasswordError(post.id) + ) + .ok(); + continue; + } + + if form.remove_posts.is_some() { + post.delete(&ctx).await?; + posts_removed += 1; + } + + if form.remove_files.is_some() { + if (post.thread.is_none() && board.config.0.require_thread_file) + || (post.thread.is_some() && board.config.0.require_reply_file) + { + writeln!(&mut response, "[Chyba] Soubor je na tomto místě potřebný.").ok(); + } else { + post.delete_files(&ctx).await?; + files_removed += post.files.0.len(); + } + } + + if form.toggle_spoiler.is_some() { + post.update_spoiler(&ctx).await?; + spoilers_toggled += post.files.0.len(); + } + } + + if posts_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny příspěvky: {posts_removed}" + ) + .ok(); + } + + if files_removed != 0 { + writeln!( + &mut response, + "[Úspěch] Odstraněny soubory: {files_removed}" + ) + .ok(); + } + + if spoilers_toggled != 0 { + writeln!( + &mut response, + "[Úspěch] Přepnuty spoilery: {spoilers_toggled}" + ) + .ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/board.rs b/src/web/board.rs new file mode 100755 index 0000000..d047743 --- /dev/null +++ b/src/web/board.rs @@ -0,0 +1,74 @@ +use actix_web::{ + get, + web::{Data, Path, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use redis::AsyncCommands; +use serde::Deserialize; + +use crate::{ + check_page, + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, paginate, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Deserialize)] +pub struct BoardQuery { + page: i64, +} + +#[derive(Template)] +#[template(path = "board.html")] +struct BoardTemplate { + tcx: TemplateCtx, + board: Board, + threads: Vec<(Post, Vec)>, + page: i64, + pages: i64, +} + +#[get("/boards/{board}")] +pub async fn board( + ctx: Data, + req: HttpRequest, + path: Path, + query: Option>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let count = ctx + .cache() + .get(format!("board_threads:{}", board.id)) + .await?; + let page = query.map_or(1, |q| q.page); + let pages = paginate(board.config.0.page_size, count); + + check_page(page, pages, board.config.0.page_count)?; + + let mut threads = Vec::new(); + + for thread in Post::read_board_page(&ctx, &board, page).await? { + let replies = thread.read_replies(&ctx).await?; + + threads.push((thread, replies)); + } + + let template = BoardTemplate { + tcx, + board, + threads, + page, + pages, + }; + + template_response(&template) +} diff --git a/src/web/board_catalog.rs b/src/web/board_catalog.rs new file mode 100644 index 0000000..31b7584 --- /dev/null +++ b/src/web/board_catalog.rs @@ -0,0 +1,46 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "board-catalog.html")] +struct BoardCatalogTemplate { + tcx: TemplateCtx, + board: Board, + threads: Vec, +} + +#[get("/boards/{board}/catalog")] +pub async fn board_catalog( + ctx: Data, + req: HttpRequest, + path: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let threads = Post::read_board_catalog(&ctx, board.id.clone()).await?; + + let template = BoardCatalogTemplate { + tcx, + board, + threads, + }; + + template_response(&template) +} diff --git a/src/web/captcha.rs b/src/web/captcha.rs new file mode 100644 index 0000000..1eb8c9f --- /dev/null +++ b/src/web/captcha.rs @@ -0,0 +1,55 @@ +use ::captcha::{gen, Difficulty}; +use actix_web::{ + get, + web::{Data, Json, Query}, +}; +use redis::AsyncCommands; +use serde::{Deserialize, Serialize}; +use sha256::digest; + +use crate::{ctx::Ctx, db::models::Board, error::NekrochanError}; + +#[derive(Deserialize)] +pub struct CaptchaQuery { + pub board: String, + pub reply: bool, +} + +#[derive(Serialize)] +pub struct CaptchaResponse { + pub png: String, + pub id: String, +} + +#[get("/captcha")] +pub async fn captcha( + ctx: Data, + Query(query): Query, +) -> Result, NekrochanError> { + let board = Board::read(&ctx, query.board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(query.board))?; + + let captcha = match board.config.thread_captcha.as_str() { + "easy" => gen(Difficulty::Easy), + "medium" => gen(Difficulty::Medium), + "hard" => gen(Difficulty::Hard), + _ => return Err(NekrochanError::NoCaptchaError), + }; + + // >YOU NEED TO MAKE A NEW ERROR TYPE FOR THIS ERROR THAT CAN ONLY HAPPEN ONCE IN THE CODE OR HOWEVER THE TRANS CHILDREN ARE PROTECTED + let png = captcha.as_base64().ok_or(NekrochanError::NoCaptchaError)?; + + let board = board.id; + let id = digest(png.as_bytes()); + + let key = format!("captcha:{board}:{id}"); + let solution = captcha.chars_as_string(); + + ctx.cache().set(&key, solution).await?; + ctx.cache().expire(&key, 600).await?; + + let res = CaptchaResponse { png, id }; + + Ok(Json(res)) +} diff --git a/src/web/edit_posts.rs b/src/web/edit_posts.rs new file mode 100644 index 0000000..e9fabbc --- /dev/null +++ b/src/web/edit_posts.rs @@ -0,0 +1,42 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use askama::Template; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + qsform::QsForm, + web::{actions::get_posts_from_ids, template_response, TemplateCtx}, +}; + +#[derive(Deserialize)] +pub struct EditPostsForm { + #[serde(default)] + pub posts: Vec, +} + +#[derive(Template)] +#[template(path = "edit-posts.html")] +struct EditPostsTemplate { + tcx: TemplateCtx, + posts: Vec, +} + +#[post("/edit-posts")] +pub async fn edit_posts( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.edit_posts()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let posts = get_posts_from_ids(&ctx, &form.posts).await; + let template = EditPostsTemplate { tcx, posts }; + + template_response(&template) +} diff --git a/src/web/index.rs b/src/web/index.rs new file mode 100755 index 0000000..7c5a97f --- /dev/null +++ b/src/web/index.rs @@ -0,0 +1,43 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use super::tcx::TemplateCtx; +use crate::{ + ctx::Ctx, + db::models::{Board, LocalStats, NewsPost}, + error::NekrochanError, + filters, + web::template_response, +}; + +#[derive(Template)] +#[template(path = "index.html")] + +struct IndexTemplate { + tcx: TemplateCtx, + news: Option, + boards: Vec, + stats: LocalStats, +} + +#[get("/")] +pub async fn index(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if tcx.boards.is_empty() { + return Err(NekrochanError::HomePageError); + } + + let news = NewsPost::read_latest(&ctx).await?; + let boards = Board::read_all(&ctx).await?; + let stats = LocalStats::read(&ctx).await?; + + let template = IndexTemplate { + tcx, + boards, + stats, + news, + }; + + template_response(&template) +} diff --git a/src/web/ip_posts.rs b/src/web/ip_posts.rs new file mode 100644 index 0000000..db3a1e5 --- /dev/null +++ b/src/web/ip_posts.rs @@ -0,0 +1,65 @@ +use actix_web::{ + get, + web::{Data, Path, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use serde::Deserialize; +use std::{collections::HashMap, net::IpAddr}; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Deserialize)] +pub struct IpPostsQuery { + page: i64, +} + +#[derive(Template)] +#[template(path = "ip-posts.html")] +struct IpPostsTemplate { + tcx: TemplateCtx, + ip: IpAddr, + boards: HashMap, + posts: Vec, + page: i64, +} + +#[get("/ip-posts/{ip}")] +pub async fn ip_posts( + ctx: Data, + req: HttpRequest, + path: Path, + query: Option>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.view_ips()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let ip = path.into_inner(); + let boards = Board::read_all_map(&ctx).await?; + let page = query.map_or(1, |q| q.page); + + if page <= 0 { + return Err(NekrochanError::InvalidPageError); + } + + let posts = Post::read_ip_page(&ctx, ip, page).await?; + + let template = IpPostsTemplate { + tcx, + ip, + boards, + posts, + page, + }; + + template_response(&template) +} diff --git a/src/web/live.rs b/src/web/live.rs new file mode 100644 index 0000000..bb5c574 --- /dev/null +++ b/src/web/live.rs @@ -0,0 +1,62 @@ +use actix_web::{ + get, + web::{Data, Path, Payload}, + HttpRequest, HttpResponse, +}; +use actix_web_actors::ws; +use uuid::Uuid; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + live_hub::TargetedPostCreatedMessage, + live_session::LiveSession, + web::tcx::TemplateCtx, +}; + +#[get("/live/{board}/{id}/{last}")] +pub async fn live( + ctx: Data, + req: HttpRequest, + path: Path<(String, i64, i64)>, + stream: Payload, +) -> Result { + let (board, id, last) = path.into_inner(); + + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let post = Post::read(&ctx, board.id.clone(), id) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?; + + if post.thread.is_some() { + return Err(NekrochanError::IsReplyError); + } + + let uuid = Uuid::new_v4(); + let thread = (board.id, id); + let tcx = TemplateCtx::new(&ctx, &req).await?; + let hub = ctx.hub(); + + let ws = LiveSession { + uuid, + thread, + tcx, + hub, + }; + + let res = ws::start(ws, &req, stream)?; + + let new_replies = post.read_replies_after(&ctx, last).await?; + + for post in new_replies { + ctx.hub() + .send(TargetedPostCreatedMessage { uuid, post }) + .await?; + } + + Ok(res) +} diff --git a/src/web/login.rs b/src/web/login.rs new file mode 100755 index 0000000..f717a7e --- /dev/null +++ b/src/web/login.rs @@ -0,0 +1,59 @@ +use actix_web::{ + cookie::Cookie, get, http::StatusCode, post, web::Data, HttpRequest, HttpResponse, + HttpResponseBuilder, +}; +use askama::Template; +use pwhash::bcrypt::verify; +use serde::Deserialize; + +use crate::{ + auth::Claims, + ctx::Ctx, + db::models::Account, + error::NekrochanError, + qsform::QsForm, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "login.html")] +struct LogInTemplate { + tcx: TemplateCtx, +} + +#[get("/login")] +pub async fn login_get(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let template = LogInTemplate { tcx }; + + template_response(&template) +} + +#[derive(Deserialize)] +pub struct LogInForm { + username: String, + password: String, +} + +#[post("/login")] +pub async fn login_post( + ctx: Data, + QsForm(form): QsForm, +) -> Result { + let account = Account::read(&ctx, form.username.clone()) + .await? + .ok_or(NekrochanError::IncorrectCredentialError)?; + + if !verify(form.password, &account.password) { + return Err(NekrochanError::IncorrectCredentialError); + } + + let auth = Claims::new(account.username).encode(&ctx)?; + + let res = HttpResponseBuilder::new(StatusCode::SEE_OTHER) + .append_header(("Location", "/staff/account")) + .cookie(Cookie::new("auth", auth)) + .finish(); + + Ok(res) +} diff --git a/src/web/logout.rs b/src/web/logout.rs new file mode 100755 index 0000000..f9c8d06 --- /dev/null +++ b/src/web/logout.rs @@ -0,0 +1,13 @@ +use actix_web::{cookie::Cookie, get, http::StatusCode, HttpResponse, HttpResponseBuilder}; + +#[get("/logout")] +pub async fn logout() -> HttpResponse { + let mut auth = Cookie::named("auth"); + + auth.make_removal(); + + HttpResponseBuilder::new(StatusCode::SEE_OTHER) + .append_header(("Location", "/")) + .cookie(auth) + .finish() +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100755 index 0000000..25a4240 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,53 @@ +pub mod actions; +pub mod board; +pub mod board_catalog; +pub mod captcha; +pub mod edit_posts; +pub mod index; +pub mod ip_posts; +pub mod live; +pub mod login; +pub mod logout; +pub mod news; +pub mod overboard; +pub mod overboard_catalog; +pub mod page; +pub mod search; +pub mod staff; +pub mod tcx; +pub mod thread; +pub mod thread_json; + +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder}; +use askama::Template; + +use self::tcx::TemplateCtx; +use crate::{ctx::Ctx, db::models::Ban, error::NekrochanError, filters}; + +#[derive(Template)] +#[template(path = "banned.html")] +struct BannedTemplate { + tcx: TemplateCtx, + ban: Ban, +} + +pub async fn ban_response( + ctx: &Ctx, + req: &HttpRequest, + ban: Ban, +) -> Result { + let tcx = TemplateCtx::new(ctx, req).await?; + + template_response(&BannedTemplate { tcx, ban }) +} + +pub fn template_response(template: &T) -> Result +where + T: Template, +{ + let res = HttpResponseBuilder::new(StatusCode::OK) + .append_header(("Content-Type", "text/html")) + .body(template.render()?); + + Ok(res) +} diff --git a/src/web/news.rs b/src/web/news.rs new file mode 100644 index 0000000..494942e --- /dev/null +++ b/src/web/news.rs @@ -0,0 +1,21 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use super::{tcx::TemplateCtx, template_response}; +use crate::{ctx::Ctx, db::models::NewsPost, error::NekrochanError, filters}; + +#[derive(Template)] +#[template(path = "news.html")] +struct NewsTemplate { + tcx: TemplateCtx, + news: Vec, +} + +#[get("/news")] +pub async fn news(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let news = NewsPost::read_all(&ctx).await?; + let template = NewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/overboard.rs b/src/web/overboard.rs new file mode 100644 index 0000000..82bbe16 --- /dev/null +++ b/src/web/overboard.rs @@ -0,0 +1,68 @@ +use actix_web::{ + get, + web::{Data, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use redis::AsyncCommands; +use serde::Deserialize; +use std::collections::HashMap; + +use crate::{ + check_page, + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, paginate, + web::{tcx::TemplateCtx, template_response}, + GENERIC_PAGE_SIZE, +}; + +#[derive(Deserialize)] +pub struct OverboardQuery { + page: i64, +} + +#[derive(Template)] +#[template(path = "overboard.html")] +struct OverboardTemplate { + tcx: TemplateCtx, + boards: HashMap, + threads: Vec<(Post, Vec)>, + page: i64, + pages: i64, +} + +#[get("/overboard")] +pub async fn overboard( + ctx: Data, + req: HttpRequest, + query: Option>, +) -> Result { + let boards = Board::read_all_map(&ctx).await?; + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let count = ctx.cache().get("total_threads").await?; + let page = query.map_or(1, |q| q.page); + let pages = paginate(GENERIC_PAGE_SIZE, count); + + check_page(page, pages, None)?; + + let mut threads = Vec::new(); + + for thread in Post::read_overboard_page(&ctx, page).await? { + let replies = thread.read_replies(&ctx).await?; + + threads.push((thread, replies)); + } + + let template = OverboardTemplate { + tcx, + boards, + threads, + page, + pages, + }; + + template_response(&template) +} diff --git a/src/web/overboard_catalog.rs b/src/web/overboard_catalog.rs new file mode 100644 index 0000000..afef01e --- /dev/null +++ b/src/web/overboard_catalog.rs @@ -0,0 +1,30 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Post, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "overboard-catalog.html")] +struct OverboardCatalogTemplate { + tcx: TemplateCtx, + threads: Vec, +} + +#[get("/overboard/catalog")] +pub async fn overboard_catalog( + ctx: Data, + req: HttpRequest, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let threads = Post::read_overboard_catalog(&ctx).await?; + + let template = OverboardCatalogTemplate { tcx, threads }; + + template_response(&template) +} diff --git a/src/web/page.rs b/src/web/page.rs new file mode 100644 index 0000000..06d3f6b --- /dev/null +++ b/src/web/page.rs @@ -0,0 +1,36 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use tokio::fs::read_to_string; + +use crate::{ctx::Ctx, error::NekrochanError, web::template_response}; + +use super::tcx::TemplateCtx; + +#[derive(Template)] +#[template(path = "page.html")] +struct PageTemplate { + pub tcx: TemplateCtx, + pub name: String, + pub content: String, +} + +#[get("/page/{name}")] +pub async fn page( + ctx: Data, + req: HttpRequest, + name: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let name = name.into_inner(); + let content = read_to_string(format!("./pages/{name}.html")) + .await + .map_err(|_| NekrochanError::PageNotFound(name.clone()))?; + + let template = PageTemplate { tcx, name, content }; + + template_response(&template) +} diff --git a/src/web/search.rs b/src/web/search.rs new file mode 100644 index 0000000..10e1b25 --- /dev/null +++ b/src/web/search.rs @@ -0,0 +1,88 @@ +use actix_web::{ + get, + web::{Data, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use serde::Deserialize; +use std::collections::HashMap; + +use super::tcx::TemplateCtx; +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, web::template_response, +}; + +#[derive(Template)] +#[template(path = "search.html")] +struct SearchTemplate { + tcx: TemplateCtx, + board_opt: Option, + boards: HashMap, + query: String, + posts: Vec, + page: i64, +} + +#[derive(Deserialize)] +pub struct SearchQuery { + board: Option, + query: String, + page: Option, +} + +#[get("/search")] +pub async fn search( + ctx: Data, + req: HttpRequest, + Query(query): Query, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board_opt = if let Some(board) = query.board { + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + Some(board) + } else { + None + }; + + let boards = if board_opt.is_none() { + Board::read_all_map(&ctx).await? + } else { + HashMap::new() + }; + + let page = query.page.unwrap_or(1); + + if page <= 0 { + return Err(NekrochanError::InvalidPageError); + } + + let query = query.query; + + if query.is_empty() || query.len() > 256 { + return Err(NekrochanError::QueryFormatError); + } + + let posts = if let Some(board) = &board_opt { + Post::read_by_query(&ctx, board, query.clone(), page).await? + } else { + Post::read_by_query_overboard(&ctx, query.clone(), page).await? + }; + + let template = SearchTemplate { + tcx, + board_opt, + boards, + query, + posts, + page, + }; + + template_response(&template) +} diff --git a/src/web/staff/account.rs b/src/web/staff/account.rs new file mode 100755 index 0000000..fcf4133 --- /dev/null +++ b/src/web/staff/account.rs @@ -0,0 +1,28 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + web::{ + tcx::{account_from_auth, TemplateCtx}, + template_response, + }, +}; + +#[derive(Template)] +#[template(path = "staff/account.html")] +struct AccountTemplate { + tcx: TemplateCtx, + account: Account, +} + +#[get("/staff/account")] +pub async fn account(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + let account = account_from_auth(&ctx, &req).await?; + let template = AccountTemplate { tcx, account }; + + template_response(&template) +} diff --git a/src/web/staff/accounts.rs b/src/web/staff/accounts.rs new file mode 100755 index 0000000..e5ffd4e --- /dev/null +++ b/src/web/staff/accounts.rs @@ -0,0 +1,31 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/accounts.html")] +struct AccountsTemplate { + tcx: TemplateCtx, + accounts: Vec, +} + +#[get("/staff/accounts")] +pub async fn accounts(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if tcx.account.is_none() { + return Err(NekrochanError::NotLoggedInError); + } + + let accounts = Account::read_all(&ctx).await?; + let template = AccountsTemplate { tcx, accounts }; + + template_response(&template) +} diff --git a/src/web/staff/actions/add_banners.rs b/src/web/staff/actions/add_banners.rs new file mode 100755 index 0000000..5e97775 --- /dev/null +++ b/src/web/staff/actions/add_banners.rs @@ -0,0 +1,42 @@ +use actix_multipart::form::{tempfile::TempFile, MultipartForm}; +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; + +use crate::{ + ctx::Ctx, + db::models::{Banner, File}, + error::NekrochanError, + web::tcx::account_from_auth, +}; + +#[derive(MultipartForm)] +pub struct AddBannersForm { + #[multipart(rename = "files[]")] + files: Vec, +} + +#[post("/staff/actions/add-banners")] +pub async fn add_banners( + ctx: Data, + req: HttpRequest, + MultipartForm(form): MultipartForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut cfg = ctx.cfg.clone(); + + cfg.files.videos = false; + + for file in form.files { + Banner::create(&ctx, File::new(&cfg, file, false, false).await?).await?; + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/banners")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/change_password.rs b/src/web/staff/actions/change_password.rs new file mode 100755 index 0000000..0c7f62c --- /dev/null +++ b/src/web/staff/actions/change_password.rs @@ -0,0 +1,38 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::{hash, verify}; +use serde::Deserialize; + +use crate::{ctx::Ctx, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth}; + +#[derive(Deserialize)] +pub struct ChangePasswordForm { + old_password: String, + new_password: String, +} + +#[post("/staff/actions/change-password")] +pub async fn change_password( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !verify(form.old_password, &account.password) { + return Err(NekrochanError::IncorrectCredentialError); + } + + if form.new_password.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(form.new_password)?; + + account.update_password(&ctx, password).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/account")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/create_account.rs b/src/web/staff/actions/create_account.rs new file mode 100755 index 0000000..e86b8c6 --- /dev/null +++ b/src/web/staff/actions/create_account.rs @@ -0,0 +1,49 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use pwhash::bcrypt::hash; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct CreateAccountForm { + username: String, + #[serde(rename = "account_password")] + password: String, +} + +#[post("/staff/actions/create-account")] +pub async fn create_account( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let username = form.username.trim().to_owned(); + let password = form.password.trim().to_owned(); + + if username.is_empty() || username.len() > 32 { + return Err(NekrochanError::UsernameFormatError); + } + + if password.len() < 8 { + return Err(NekrochanError::PasswordFormatError); + } + + let password = hash(password)?; + + let _ = Account::create(&ctx, username, password).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/create_board.rs b/src/web/staff/actions/create_board.rs new file mode 100755 index 0000000..d24ec98 --- /dev/null +++ b/src/web/staff/actions/create_board.rs @@ -0,0 +1,56 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use lazy_static::lazy_static; +use regex::Regex; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +lazy_static! { + static ref ID_REGEX: Regex = Regex::new(r"^\w{1,16}$").unwrap(); +} + +#[derive(Deserialize)] +pub struct CreateBoardForm { + id: String, + name: String, + description: String, +} + +#[post("/staff/actions/create-board")] +pub async fn create_board( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let id = form.id.trim().to_owned(); + let name = form.name.trim().to_owned(); + let description = form.description.trim().to_owned(); + + if !ID_REGEX.is_match(&id) { + return Err(NekrochanError::IdFormatError); + } + + if name.is_empty() || name.len() > 32 { + return Err(NekrochanError::BoardNameFormatError); + } + + if description.len() > 128 { + return Err(NekrochanError::DescriptionFormatError); + } + + let _ = Board::create(&ctx, id, name, description).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/create_news.rs b/src/web/staff/actions/create_news.rs new file mode 100644 index 0000000..0b1a07f --- /dev/null +++ b/src/web/staff/actions/create_news.rs @@ -0,0 +1,48 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::NewsPost, error::NekrochanError, markup::markup, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct CreateNewsForm { + title: String, + content: String, +} + +#[post("/staff/actions/create-news")] +pub async fn create_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let title = form.title.trim().to_owned(); + let content = form.content.trim().to_owned(); + + if title.is_empty() || title.len() > 100 { + return Err(NekrochanError::NewsTitleFormatError); + } + + if content.is_empty() || content.len() > 10000 { + return Err(NekrochanError::NewsContentFormatError); + } + + let content_nomarkup = content; + let (content, _) = markup(&ctx, &account.perms(), None, None, &content_nomarkup).await?; + + NewsPost::create(&ctx, title, content, content_nomarkup, account.username).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/news")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/delete_account.rs b/src/web/staff/actions/delete_account.rs new file mode 100755 index 0000000..1857a9b --- /dev/null +++ b/src/web/staff/actions/delete_account.rs @@ -0,0 +1,23 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; + +use crate::{ctx::Ctx, error::NekrochanError, web::tcx::account_from_auth}; + +#[post("/staff/actions/delete-account")] +pub async fn delete_account( + ctx: Data, + req: HttpRequest, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if account.perms().owner() { + return Err(NekrochanError::OwnerDeletionError); + } + + account.delete(&ctx).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/logout")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/edit_news.rs b/src/web/staff/actions/edit_news.rs new file mode 100644 index 0000000..a38ce56 --- /dev/null +++ b/src/web/staff/actions/edit_news.rs @@ -0,0 +1,71 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use std::{collections::HashMap, fmt::Write}; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + markup::markup, + qsform::QsForm, + web::{actions::ActionTemplate, tcx::TemplateCtx, template_response}, +}; + +#[post("/staff/actions/edit-news")] +pub async fn edit_news( + ctx: Data, + req: HttpRequest, + QsForm(edits): QsForm>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in edits.keys() { + if let Some(newspost) = NewsPost::read(&ctx, *id).await? { + news.push(newspost); + } + } + + let news = news + .into_iter() + .map(|newspost| (newspost.id, newspost)) + .collect::>(); + + let mut response = String::new(); + let mut news_edited = 0; + + for (id, content_nomarkup) in edits { + let newspost = &news[&id]; + + if !tcx.perms.owner() && tcx.account != Some(newspost.author.clone()) { + writeln!( + &mut response, + "[Chyba] pouze vlastník nebo autor může upravit novinky." + ) + .ok(); + + continue; + } + + let content_nomarkup = content_nomarkup.trim(); + let (content, _) = markup(&ctx, &tcx.perms, None, None, content_nomarkup).await?; + + newspost + .update(&ctx, content, content_nomarkup.into()) + .await?; + + news_edited += 1; + } + + if news_edited != 0 { + writeln!(&mut response, "[Úspěch] Upraveny novinky: {news_edited}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/staff/actions/mod.rs b/src/web/staff/actions/mod.rs new file mode 100755 index 0000000..cb33f4f --- /dev/null +++ b/src/web/staff/actions/mod.rs @@ -0,0 +1,16 @@ +pub mod add_banners; +pub mod change_password; +pub mod create_account; +pub mod create_board; +pub mod create_news; +pub mod delete_account; +pub mod edit_news; +pub mod remove_accounts; +pub mod remove_banners; +pub mod remove_bans; +pub mod remove_boards; +pub mod remove_news; +pub mod transfer_ownership; +pub mod update_board_config; +pub mod update_boards; +pub mod update_permissions; diff --git a/src/web/staff/actions/remove_accounts.rs b/src/web/staff/actions/remove_accounts.rs new file mode 100755 index 0000000..6c63336 --- /dev/null +++ b/src/web/staff/actions/remove_accounts.rs @@ -0,0 +1,42 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveAccountsForm { + #[serde(default)] + accounts: Vec, +} + +#[post("/staff/actions/remove-accounts")] +pub async fn remove_accounts( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + for account in form.accounts { + if let Some(account) = Account::read(&ctx, account).await? { + if account.owner { + return Err(NekrochanError::OwnerDeletionError); + } + + account.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_banners.rs b/src/web/staff/actions/remove_banners.rs new file mode 100755 index 0000000..9be373b --- /dev/null +++ b/src/web/staff/actions/remove_banners.rs @@ -0,0 +1,38 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Banner, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBannersForm { + #[serde(default)] + banners: Vec, +} + +#[post("/staff/actions/remove-banners")] +pub async fn remove_banners( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + for id in form.banners { + if let Some(banner) = Banner::read(&ctx, id).await? { + banner.remove(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/banners")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_bans.rs b/src/web/staff/actions/remove_bans.rs new file mode 100755 index 0000000..79f7736 --- /dev/null +++ b/src/web/staff/actions/remove_bans.rs @@ -0,0 +1,37 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Ban, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBansForm { + #[serde(default)] + bans: Vec, +} + +#[post("/staff/actions/remove-bans")] +pub async fn remove_bans( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().bans()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + for ban in form.bans { + if let Some(ban) = Ban::read_by_id(&ctx, ban).await? { + ban.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/bans")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_boards.rs b/src/web/staff/actions/remove_boards.rs new file mode 100755 index 0000000..382ea71 --- /dev/null +++ b/src/web/staff/actions/remove_boards.rs @@ -0,0 +1,37 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct RemoveBoardsForm { + #[serde(default)] + boards: Vec, +} + +#[post("/staff/actions/remove-boards")] +pub async fn remove_boards( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + for board in form.boards { + if let Some(board) = Board::read(&ctx, board).await? { + board.delete(&ctx).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/remove_news.rs b/src/web/staff/actions/remove_news.rs new file mode 100644 index 0000000..f0aedef --- /dev/null +++ b/src/web/staff/actions/remove_news.rs @@ -0,0 +1,65 @@ +use std::fmt::Write; + +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + qsform::QsForm, + web::{actions::ActionTemplate, template_response, TemplateCtx}, +}; + +#[derive(Deserialize)] +pub struct RemoveNewsForm { + #[serde(default)] + pub news: Vec, +} + +#[post("/staff/actions/remove-news")] +pub async fn remove_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in form.news { + if let Some(newspost) = NewsPost::read(&ctx, id).await? { + news.push(newspost); + } + } + + let mut response = String::new(); + let mut news_removed = 0; + + for newspost in news { + if !tcx.perms.owner() && tcx.account != Some(newspost.author.clone()) { + writeln!( + &mut response, + "[Chyba] pouze vlastník nebo autor může odstranit novinky." + ) + .ok(); + + continue; + } + + newspost.delete(&ctx).await?; + news_removed += 1; + } + + if news_removed != 0 { + writeln!(&mut response, "[Úspěch] Odstraněny novinky: {news_removed}").ok(); + } + + let template = ActionTemplate { tcx, response }; + + template_response(&template) +} diff --git a/src/web/staff/actions/transfer_ownership.rs b/src/web/staff/actions/transfer_ownership.rs new file mode 100755 index 0000000..bbd3103 --- /dev/null +++ b/src/web/staff/actions/transfer_ownership.rs @@ -0,0 +1,39 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct TransferOwnershipForm { + account: String, +} + +#[post("/staff/actions/transfer-ownership")] +pub async fn transfer_ownership( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let old_owner = account_from_auth(&ctx, &req).await?; + + if !old_owner.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let new_owner = form.account; + let new_owner = Account::read(&ctx, new_owner.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(new_owner))?; + + old_owner.update_owner(&ctx, false).await?; + new_owner.update_owner(&ctx, true).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/account")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_board_config.rs b/src/web/staff/actions/update_board_config.rs new file mode 100755 index 0000000..bce3864 --- /dev/null +++ b/src/web/staff/actions/update_board_config.rs @@ -0,0 +1,105 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use serde::Deserialize; + +use crate::{ + cfg::BoardCfg, ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct UpdateBoardConfigForm { + board: String, + anon_name: String, + page_size: i64, + page_count: i64, + file_limit: usize, + bump_limit: i32, + reply_limit: i32, + locked: Option, + user_ids: Option, + flags: Option, + thread_captcha: String, + reply_captcha: String, + board_theme: String, + require_thread_content: Option, + require_thread_file: Option, + require_reply_content: Option, + require_reply_file: Option, + antispam: Option, + antispam_ip: i64, + antispam_content: i64, + antispam_both: i64, + thread_cooldown: i64, +} + +#[post("/staff/actions/update-board-config")] +pub async fn update_board_config( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !(account.perms().owner() || account.perms().board_config()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = form.board; + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let anon_name = form.anon_name; + let page_size = form.page_size; + let page_count = form.page_count; + let file_limit = form.file_limit; + let bump_limit = form.bump_limit; + let reply_limit = form.reply_limit; + let locked = form.locked.is_some(); + let user_ids = form.user_ids.is_some(); + let flags = form.flags.is_some(); + let thread_captcha = form.thread_captcha; + let reply_captcha = form.reply_captcha; + let board_theme = form.board_theme; + let require_thread_content = form.require_thread_content.is_some(); + let require_thread_file = form.require_thread_file.is_some(); + let require_reply_content = form.require_reply_content.is_some(); + let require_reply_file = form.require_reply_file.is_some(); + let antispam = form.antispam.is_some(); + let antispam_ip = form.antispam_ip; + let antispam_content = form.antispam_content; + let antispam_both = form.antispam_both; + let thread_cooldown = form.thread_cooldown; + + let config = BoardCfg { + anon_name, + page_size, + page_count, + file_limit, + bump_limit, + reply_limit, + locked, + user_ids, + flags, + thread_captcha, + reply_captcha, + board_theme, + require_thread_content, + require_thread_file, + require_reply_content, + require_reply_file, + antispam, + antispam_ip, + antispam_content, + antispam_both, + thread_cooldown, + }; + + board.update_config(&ctx, config).await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_boards.rs b/src/web/staff/actions/update_boards.rs new file mode 100755 index 0000000..77f6b92 --- /dev/null +++ b/src/web/staff/actions/update_boards.rs @@ -0,0 +1,57 @@ +use actix_web::{ + post, + web::{Bytes, Data}, + HttpRequest, HttpResponse, +}; +use serde::Deserialize; +use serde_qs::Config; + +use crate::{ctx::Ctx, db::models::Board, error::NekrochanError, web::tcx::account_from_auth}; + +#[derive(Deserialize)] +pub struct UpdateBoardsForm { + #[serde(default)] + boards: Vec, + name: String, + description: String, +} + +#[post("/staff/actions/update-boards")] +pub async fn update_boards( + ctx: Data, + req: HttpRequest, + bytes: Bytes, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let config = Config::new(10, false); + let form: UpdateBoardsForm = config.deserialize_bytes(&bytes)?; + + let name = form.name.trim().to_owned(); + let description = form.description.trim().to_owned(); + + if name.is_empty() || name.len() > 32 { + return Err(NekrochanError::BoardNameFormatError); + } + + if description.len() > 128 { + return Err(NekrochanError::DescriptionFormatError); + } + + for board in form.boards { + if let Some(board) = Board::read(&ctx, board).await? { + board.update_name(&ctx, name.clone()).await?; + board.update_description(&ctx, description.clone()).await?; + } + } + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/boards")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/actions/update_permissions.rs b/src/web/staff/actions/update_permissions.rs new file mode 100755 index 0000000..c20f32d --- /dev/null +++ b/src/web/staff/actions/update_permissions.rs @@ -0,0 +1,128 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use enumflags2::BitFlags; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, db::models::Account, error::NekrochanError, perms::Permissions, qsform::QsForm, + web::tcx::account_from_auth, +}; + +#[derive(Deserialize)] +pub struct UpdatePermissionsForm { + account: String, + edit_posts: Option, + manage_posts: Option, + capcodes: Option, + custom_capcodes: Option, + staff_log: Option, + reports: Option, + bans: Option, + banners: Option, + board_config: Option, + news: Option, + jannytext: Option, + view_ips: Option, + bypass_bans: Option, + bypass_board_lock: Option, + bypass_thread_lock: Option, + bypass_captcha: Option, + bypass_antispam: Option, +} + +#[post("/staff/actions/update-permissions")] +pub async fn update_permissions( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let account = account_from_auth(&ctx, &req).await?; + + if !account.perms().owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let updated_account = form.account; + let updated_account = Account::read(&ctx, updated_account.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(updated_account))?; + + let mut permissions = BitFlags::empty(); + + if form.edit_posts.is_some() { + permissions |= Permissions::EditPosts; + } + + if form.manage_posts.is_some() { + permissions |= Permissions::ManagePosts; + } + + if form.capcodes.is_some() { + permissions |= Permissions::Capcodes; + } + + if form.custom_capcodes.is_some() { + permissions |= Permissions::CustomCapcodes; + } + + if form.staff_log.is_some() { + permissions |= Permissions::StaffLog; + } + + if form.reports.is_some() { + permissions |= Permissions::Reports; + } + + if form.bans.is_some() { + permissions |= Permissions::Bans; + } + + if form.banners.is_some() { + permissions |= Permissions::BoardBanners; + } + + if form.board_config.is_some() { + permissions |= Permissions::BoardConfig; + } + + if form.news.is_some() { + permissions |= Permissions::News; + } + + if form.jannytext.is_some() { + permissions |= Permissions::Jannytext; + } + + if form.view_ips.is_some() { + permissions |= Permissions::ViewIPs; + } + + if form.bypass_bans.is_some() { + permissions |= Permissions::BypassBans; + } + + if form.bypass_board_lock.is_some() { + permissions |= Permissions::BypassBoardLock; + } + + if form.bypass_thread_lock.is_some() { + permissions |= Permissions::BypassThreadLock; + } + + if form.bypass_captcha.is_some() { + permissions |= Permissions::BypassCaptcha; + } + + if form.bypass_antispam.is_some() { + permissions |= Permissions::BypassAntispam; + } + + updated_account + .update_permissions(&ctx, permissions.bits()) + .await?; + + let res = HttpResponse::SeeOther() + .append_header(("Location", "/staff/accounts")) + .finish(); + + Ok(res) +} diff --git a/src/web/staff/banners.rs b/src/web/staff/banners.rs new file mode 100755 index 0000000..1b651d7 --- /dev/null +++ b/src/web/staff/banners.rs @@ -0,0 +1,30 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Banner, + error::NekrochanError, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/banners.html")] +struct BannersTemplate { + tcx: TemplateCtx, + banners: Vec, +} + +#[get("/staff/banners")] +pub async fn banners(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let banners = Banner::read_all(&ctx).await?; + let template = BannersTemplate { tcx, banners }; + + template_response(&template) +} diff --git a/src/web/staff/bans.rs b/src/web/staff/bans.rs new file mode 100755 index 0000000..70d1efd --- /dev/null +++ b/src/web/staff/bans.rs @@ -0,0 +1,31 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Ban, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/bans.html")] +struct BansTemplate { + tcx: TemplateCtx, + bans: Vec, +} + +#[get("/staff/bans")] +pub async fn bans(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.bans()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let bans = Ban::read_all(&ctx).await?; + let template = BansTemplate { tcx, bans }; + + template_response(&template) +} diff --git a/src/web/staff/board_config.rs b/src/web/staff/board_config.rs new file mode 100755 index 0000000..6da2dd6 --- /dev/null +++ b/src/web/staff/board_config.rs @@ -0,0 +1,42 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Board, + error::NekrochanError, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/board-config.html")] +struct BannersTemplate { + tcx: TemplateCtx, + board: Board, +} + +#[get("/staff/board-config/{board}")] +pub async fn board_config( + ctx: Data, + req: HttpRequest, + board: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.board_config()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let board = board.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let template = BannersTemplate { tcx, board }; + + template_response(&template) +} diff --git a/src/web/staff/boards.rs b/src/web/staff/boards.rs new file mode 100755 index 0000000..bd3b1ff --- /dev/null +++ b/src/web/staff/boards.rs @@ -0,0 +1,31 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Board, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/boards.html")] +struct BoardsTemplate { + tcx: TemplateCtx, + boards: Vec, +} + +#[get("/staff/boards")] +pub async fn boards(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.board_config() || tcx.perms.banners()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let boards = Board::read_all(&ctx).await?; + let template = BoardsTemplate { tcx, boards }; + + template_response(&template) +} diff --git a/src/web/staff/edit_news.rs b/src/web/staff/edit_news.rs new file mode 100644 index 0000000..d7f9865 --- /dev/null +++ b/src/web/staff/edit_news.rs @@ -0,0 +1,49 @@ +use actix_web::{post, web::Data, HttpRequest, HttpResponse}; +use askama::Template; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + filters, + qsform::QsForm, + web::{template_response, TemplateCtx}, +}; + +#[derive(Deserialize)] +pub struct EditNewsForm { + pub news: Vec, +} + +#[derive(Template)] +#[template(path = "staff/edit-news.html")] +struct EditNewsTemplate { + tcx: TemplateCtx, + news: Vec, +} + +#[post("/staff/edit-news")] +pub async fn edit_news( + ctx: Data, + req: HttpRequest, + QsForm(form): QsForm, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let mut news = Vec::new(); + + for id in form.news { + if let Some(newspost) = NewsPost::read(&ctx, id).await? { + news.push(newspost); + } + } + + let template = EditNewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/staff/mod.rs b/src/web/staff/mod.rs new file mode 100755 index 0000000..f61de2e --- /dev/null +++ b/src/web/staff/mod.rs @@ -0,0 +1,11 @@ +pub mod account; +pub mod accounts; +pub mod actions; +pub mod banners; +pub mod bans; +pub mod board_config; +pub mod boards; +pub mod edit_news; +pub mod news; +pub mod permissions; +pub mod reports; diff --git a/src/web/staff/news.rs b/src/web/staff/news.rs new file mode 100644 index 0000000..7bf1a61 --- /dev/null +++ b/src/web/staff/news.rs @@ -0,0 +1,31 @@ +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::NewsPost, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/news.html")] +struct NewsTemplate { + tcx: TemplateCtx, + news: Vec, +} + +#[get("/staff/news")] +pub async fn news(ctx: Data, req: HttpRequest) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.news()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let news = NewsPost::read_all(&ctx).await?; + let template = NewsTemplate { tcx, news }; + + template_response(&template) +} diff --git a/src/web/staff/permissions.rs b/src/web/staff/permissions.rs new file mode 100755 index 0000000..251ed33 --- /dev/null +++ b/src/web/staff/permissions.rs @@ -0,0 +1,42 @@ +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::Account, + error::NekrochanError, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "staff/permissions.html")] +struct PermissionsTemplate { + tcx: TemplateCtx, + account: Account, +} + +#[get("/staff/permissions/{account}")] +pub async fn permissions( + ctx: Data, + req: HttpRequest, + path: Path, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !tcx.perms.owner() { + return Err(NekrochanError::InsufficientPermissionError); + } + + let account = path.into_inner(); + let account = Account::read(&ctx, account.clone()) + .await? + .ok_or(NekrochanError::AccountNotFound(account))?; + + let template = PermissionsTemplate { tcx, account }; + + template_response(&template) +} diff --git a/src/web/staff/reports.rs b/src/web/staff/reports.rs new file mode 100755 index 0000000..3e33249 --- /dev/null +++ b/src/web/staff/reports.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use actix_web::{ + get, + web::{Data, Query}, + HttpRequest, HttpResponse, +}; +use askama::Template; +use serde::Deserialize; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Deserialize)] +pub struct BoardQuery { + page: i64, +} + +#[allow(dead_code)] +#[derive(Template)] +#[template(path = "staff/reports.html")] +struct ReportsTemplate { + tcx: TemplateCtx, + boards: HashMap, + posts: Vec, + page: i64, +} + +#[get("/staff/reports")] +async fn reports( + ctx: Data, + req: HttpRequest, + query: Option>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + if !(tcx.perms.owner() || tcx.perms.reports()) { + return Err(NekrochanError::InsufficientPermissionError); + } + + let boards = Board::read_all_map(&ctx).await?; + let page = query.map_or(1, |q| q.page); + + if page <= 0 { + return Err(NekrochanError::InvalidPageError); + } + + let posts = Post::read_reports_page(&ctx, page).await?; + + let template = ReportsTemplate { + tcx, + boards, + posts, + page, + }; + + template_response(&template) +} diff --git a/src/web/tcx.rs b/src/web/tcx.rs new file mode 100755 index 0000000..e721af0 --- /dev/null +++ b/src/web/tcx.rs @@ -0,0 +1,116 @@ +use actix_web::HttpRequest; +use redis::{AsyncCommands, Commands, Connection}; +use sqlx::query_as; +use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr}, +}; + +use crate::{ + auth::Claims, cfg::Cfg, ctx::Ctx, db::models::Account, error::NekrochanError, + perms::PermissionWrapper, +}; + +#[derive(Debug, Clone)] +pub struct TemplateCtx { + pub cfg: Cfg, + pub boards: Vec, + pub account: Option, + pub perms: PermissionWrapper, + pub ip: IpAddr, + pub yous: HashSet, + pub report_count: Option, +} + +impl TemplateCtx { + pub async fn new(ctx: &Ctx, req: &HttpRequest) -> Result { + let cfg = ctx.cfg.clone(); + let boards = ctx.cache().lrange("board_ids", 0, -1).await?; + + let account = account_from_auth_opt(ctx, req).await?; + + let perms = match &account { + Some(account) => account.perms(), + None => PermissionWrapper::new(0, false), + }; + + let (ip, _) = ip_from_req(req)?; + let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?; + + let account = account.map(|account| account.username); + + let report_count = if perms.owner() || perms.reports() { + let count: Option> = query_as("SELECT SUM(jsonb_array_length(reports)) FROM overboard WHERE reports != '[]'::jsonb") + .fetch_optional(ctx.db()) + .await + .ok(); + + match count { + Some(Some((count,))) if count != 0 => Some(count), + _ => None, + } + } else { + None + }; + + let tcx = Self { + cfg, + boards, + perms, + ip, + yous, + account, + report_count, + }; + + Ok(tcx) + } + + pub fn update_yous(&mut self, cache: &mut Connection) -> Result<(), NekrochanError> { + self.yous = cache.zrange(format!("by_ip:{}", self.ip), 0, -1)?; + Ok(()) + } +} + +pub async fn account_from_auth(ctx: &Ctx, req: &HttpRequest) -> Result { + let account = account_from_auth_opt(ctx, req) + .await? + .ok_or(NekrochanError::NotLoggedInError)?; + + Ok(account) +} + +pub async fn account_from_auth_opt( + ctx: &Ctx, + req: &HttpRequest, +) -> Result, NekrochanError> { + let account = match req.cookie("auth") { + Some(auth) => { + let claims = Claims::decode(ctx, auth.value())?; + let account = Account::read(ctx, claims.sub) + .await? + .ok_or(NekrochanError::InvalidAuthError)?; + + Some(account) + } + None => None, + }; + + Ok(account) +} + +pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> { + let ip = req + .connection_info() + .realip_remote_addr() + .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |ip| { + ip.parse().unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) + }); + + let country = req.headers().get("X-Country-Code").map_or_else( + || "xx".into(), + |hdr| hdr.to_str().unwrap_or("xx").to_ascii_lowercase(), + ); + + Ok((ip, country)) +} diff --git a/src/web/thread.rs b/src/web/thread.rs new file mode 100644 index 0000000..57a12a7 --- /dev/null +++ b/src/web/thread.rs @@ -0,0 +1,59 @@ +use actix_web::{ + get, + http::StatusCode, + web::{Data, Path}, + HttpRequest, HttpResponse, +}; +use askama::Template; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + filters, + web::{tcx::TemplateCtx, template_response}, +}; + +#[derive(Template)] +#[template(path = "thread.html")] +struct ThreadTemplate { + tcx: TemplateCtx, + board: Board, + thread: Post, + replies: Vec, +} + +#[get("/boards/{board}/{thread}")] +pub async fn thread( + ctx: Data, + req: HttpRequest, + path: Path<(String, i64)>, +) -> Result { + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let (board, id) = path.into_inner(); + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let thread = Post::read(&ctx, board.id.clone(), id) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?; + + if thread.thread.is_some() { + return Ok(HttpResponse::build(StatusCode::PERMANENT_REDIRECT) + .append_header(("Location", thread.post_url())) + .finish()); + } + + let replies = thread.read_replies(&ctx).await?; + + let template = ThreadTemplate { + tcx, + board, + thread, + replies, + }; + + template_response(&template) +} diff --git a/src/web/thread_json.rs b/src/web/thread_json.rs new file mode 100644 index 0000000..277022f --- /dev/null +++ b/src/web/thread_json.rs @@ -0,0 +1,58 @@ +use actix_web::{ + get, + web::{Data, Json, Path}, + HttpRequest, +}; +use askama::Template; +use std::{collections::HashMap, vec}; + +use crate::{ + ctx::Ctx, + db::models::{Board, Post}, + error::NekrochanError, + web::tcx::TemplateCtx, + PostTemplate, +}; + +#[get("/thread-json/{board}/{id}")] +pub async fn thread_json( + ctx: Data, + req: HttpRequest, + path: Path<(String, i64)>, +) -> Result>, NekrochanError> { + let (board, id) = path.into_inner(); + + let tcx = TemplateCtx::new(&ctx, &req).await?; + + let board = Board::read(&ctx, board.clone()) + .await? + .ok_or(NekrochanError::BoardNotFound(board))?; + + let thread = Post::read(&ctx, board.id.clone(), id) + .await? + .ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?; + + if thread.thread.is_some() { + return Err(NekrochanError::IsReplyError); + } + + let mut res = HashMap::new(); + + let replies = thread.read_replies(&ctx).await?; + let posts = [vec![thread], replies].concat(); + + for post in posts { + let id = post.id; + let tcx = &tcx; + let board = &board; + let post = &post; + + let html = PostTemplate { tcx, board, post } + .render() + .unwrap_or_default(); + + res.insert(id, html); + } + + Ok(Json(res)) +} diff --git a/static/default-banner.png b/static/default-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..e5686d5d438ba0847940c9406f069065e35c83f2 GIT binary patch literal 19816 zcmX_mWmH_jvNaksxMy&;puyeU-Q6{~OK`V9aCi3v_rcvA26uOukNe&C-mKMg&gvhh z`_!(ztGZ4_Dk(^QMI=CkfPnZaEhVM`0RhSId2a^)<@28p@@voMM5wi>sFJj(D2dZg zM+<8^a|j5UM88CSseU2AprL9p9X2d)1eaNv0*+WQ{CAyDGBx;SkxCS*G=Jgbu%f|1 zWhX|ir+O_nC+Ee5UUf7WK_NZ;EG+$^0Wf7K4hs?B5wy_>+~5@|OZ#Kt76YAOz><9Z z=g(wSRFEdjOuT~_3pv}^pX4GKKHw6+uWx7c6~m=VK%Bu3Tb3Sbj49UsDl|tlNlyAN zhkmyqJZ*RDR(5exTD`RLNrt(0hR#%=wqlaiB2jC|ihCLykrlaX5LR$xu{vFI zuth5bHkrCaLVVqG7*S(3G9x(;o4%Pm222z)bJkrQO9_!x!%j=es+qK3v9OJFK2p;y zGItn2H11cXXf@A=bOlRe zXNrC!TTu;3VdU3UHA&<`kUHH0d^rh|ECD7dPXoI|r7JIg(`-zv`ZXCWv1$r_VnxGs zYVe{fZY`fQrHE%jBBdNag&r}jp=rhl2;KMukEqJ5fePOt7RD_aM+!^;Z68c8dUbff zBS1+TA%{*eOXTYh9LU=ffWD^I#^Ha%AN=!OgJXq*f3O73f+RN5h5*+B9ul8c=K$tD zVnjz|HJVlo_f%C5uioru2tTjxUMTrI`ld%Y18RH;YM9XvEoLz!A$Q^9KB`tLhH zQ5Q)Wch!>Beq@nxlZv_OW7p>@~a+B7xL%m^oen&6IUJNr}lPt?U-B<8ZD%T3|? z6DF&+yL5e|sL>H!k66&n2pW)6aOUZf##7IZILg1z++{!pouhjSN0S(0dM^j5*)lO1lRGz3m#~Ha>1gIvLp{Y@l)F8&hOSkJ+wWvw5 zOVdbYLsKSD0lCXU)GO3SE*6ph4-9RDt~B!E5_%Uo(toB0kQo!fdHKp+c?57V@pw%N zBV!oK3S`lUvm7i`)bOz826^Tp1;`3GSOVl8+%i%z%2?u&Xw&Yg-kq|G8Za99HzE3K z9!gkYb?oy5I2HI*Gi3~Y-jR%a8}q7_-*)Tr7y!jfPZ%kIRpa;KrbF_3R#I4D zscqm3t=DJ~=P0bDFc&jk)qFH;0@N; zsngbKeQ zO$N!%&`~pGCj3evAVz>fpI%ip_U0jHg5{7V9lc%!6$23zkC8fXJ(0TZ{y>D_l=W;E zQ>(x>_o!BCTgZqz4w+PkdQUE!#lmqkr{hT{eBsz)5zX1>vXtJ1A^z<;mSU5oqz;+* z-Kj@zjP27Ek-1SEQ25a`R{oWSA~DK%n_1&ov(t=mOjDieB#{h6L(wvHIt(vXw-3uo zYR!bqnnL`7U`iatqzi2(3Mz@nPt$FvDq>(Dmne{gcDD%bIOIA}5@*GkKAFEdMmTT0gGu^ zHAeqSNS#4S4frB9hRVe=~lVF7< zQZWb>rC6cz2ygLxzj%k8YXdZt|EhG3=vqUbu?g#`6ox4E<@AI~(<4Qn(qBN8#J4)H z@duwCY)I##NH=Lam>{f_8W@;9Z*N*Ib`69R9tvxrA4)I==#uE=?CWi|)YS$-xEO}1 zc;r|7q46ZR7^Rj%lp%%Ka`c00057d1+@z>RxiIXuYMf#;8rc6e{`9{IA==dMA7ND_j~ce%b#3Do;8e1*BidC{Xhv5q4;51$9T3All$BW{M+5|= zN+Ch@FQ$LzwQM&F6~|0i!6?Scu94&lvhKC&3`!;mF@8=W$X`0MzMz;+qKGS#Abo<$ zr>HsNRQ{h07#&V~rL)wNL8d1k=vOhYOeVv82`QxEb4FG!Td7}3bZ4uG90-B^q_uDs zivT|Q5;BS;+JPr;s0vOkQi_@s`t%5_>YV;X?y*Utk~b9PmBMyN-d!rQ+|flK|KnuB zJ&BzaK_O$GRN?@7A1?;3@&dt*57wq~3P~|F?tg9YxrJi->#n)n9JSzCotOkHuOLm7 z>ii}g+7r9Kjq-jBW+O6#q>=`^QW+`@5gBK&x*}-~fi$8d9)XanBMa?L#eBgEqXiA#+W&RV8 zSoXMvxtnH!-A|@5pimbPgJp!2Ev%{&5u57IHjA`4thL%ym|GYmLkJuClvL0>BN<Yx^z#f30wtkT`1SmbF#Yn}ZBC?O(M^;2K#wYv( zbfu0GLwQe>_Ry$NpEIsoAXHdXM8ha@&-_Jib$juf<&hZ0>d8d3FE^{-?X zj#R1p+v3uGO+#nz_EmKxNM;b>)j3A}{JK}?7?|)keY*e>So34yum2@Lwy5`|GNMqP zTi_#+FWAwRU;O)#E6j+NM2R=n9@e6=1!AUG%f z^Eugus+SNJ_0tl~xmP`1wr`FmfnA3_IY-jcJNGfanCpjYp_D+$?8V29tK~^vzkcZDESjH(xqN`CF;DqQH z*IDUez-9Dtdkc_bQ<5d%F(UjHTM7Jn%J*2)m_jItrLKe?A%(%gSMD4k6m-?3L%X4n zxjC8AR#h0BMN@Y)oi7h-3e)(V8kb;)fQD3BT|rBI8(A4|o6_k2G!KIoJ~mJ1t8_H7 zBcXHA^&+c4Ntp)2EHBidH4Fx!)a?3hC(tKBL5Oq)FD7B#FMD@WchzhniHsiJl$ix4 zQp`rU_+q9_s2$O|aUh)e2Y>8cuy+DUhv-SndMlc65H@VOt!e@pMOj_+Jgs;->F**M znY=9^;eUfaXLzcJQYAr$JZ%w>Mm!yt0)giDz2ZDG>8xmmxho$`kcf07BSVl9o@u%_ ziBbOq`WgrO4B0>iij=&T+b|E-Eang*wh)$70~|#@biNSXa-&bwHTI8!VUj9pYH|9A z*t`Tv1>Pp?qr9_AJFAzqDsM+?cCr%7#{bI!i%F)Z$7IDwdV&{?Vn|{rf|6Z*DAQe} zpjBkE9*qbk=xHd|E(!PJGI{QF7+sGNsrq+js7T83^~lgf$eKi-J`T?}=O`D6NMVC! zr9kP&?$*PB6z~rYhNMa=s!zcRIjZ*rl?+rcx;!#znCdXvGorNI{|&i6QS_ssyweAv zgds-nFh}X6g>o!h0eh3msZeRMYDbaK@vgxIrpxY%!}d#x=wG6ko5Y-jg2WNwF`}8# z7ZRy9#-~9rOLE3+zB2F~nR3#(IN6$?p=ru#Hhh0iAa*Zx;7M+QuV2C4)3!k$*mNFhJ5 z6jXH3w5~D?TF)FW^F$l?BrCq(i^9k*(NGXo!xUV(>PA_V4jyTR+PGnzG8BJJ+=F55 zQGh*LpQl_*Qo+KKDWpTmmMbg>7FtrGk)WbbZO8UfPB0Ii_JlH>rjs1))NUG0+9xoC z_kRAbNtZ9a;oU zqqj0iDLg_iPwL7%K#3_rHi0tMX86CW|K*;K!hEB&wwaO13!%n>BJa_S$0DJSBAu2& zvl6y`O|RE^Y}6^fqS>Jt`@piw`7L!!s~8R;FC3^8I7nj#^q-f=A~4DnN(Af#jHD>oK}mSUuhf?CkxAR=fzUA4Cr%o{nD7+M zHhs%hp|dzNrpiMYi+r10t~4yOb(Wf_#xPcq(_syDDU%pth^LG}Fz1z??Dc49#do70 zAG80HY#dDUdgnCIAc+2AWQGZrP)7LjPd@dndHV;nBK=DNuK!MS0*`}o=o-3)8O3uM?S?MzN0*P zAK!ez9Rg$jLT$+frvfz#$`;Ri1%8nZWrUTcU=fa_$F%U!usV13JYD%`3dc4Fi>IiB zel&o-;DHuKU=2fek~>*%vJbOAL?BK(9F_i2-hi@N4ouCVNcc02UJ=-bXQ5*+H%mG7 zzwb@@-M9H>@xg_?BMqKnQ&c&H3r;~Q#wmoA#4$`vJ(5{JB?TTM<%*{GxCxM5a;Yf% zU}Qm8ywr$SEG$&|TXhfdkWqM{5K{@=6I$gLE?P*GDDdkT9eLGS8yr3C>|UAFZgWXp za2Q}vS90-$Ri$#rPllCM_V`79>3_KX)2f4NimeHQgiqXfMyD{P&nH}n#4JG3LVCEw zjj`%pDnCJF_Xr~-&Y``hJusMK^Z3JC_8i_OxL_E8jwoLiEvfDc5Bkxvl1wxsA%ujd zH;1K%BWfW%8I6gn(zG<2MVoa%=2Fj&T@X9vKgXRpI$@%~oBy>w8g3WemHF|D>hFJ| z(g=OYxN*$WTyWwSSDqT7C(s6rj_I(#6kYaIu>TvC%r9e@`tjgLIl;Ob%TnM6vh!ij@u<&=n<&#DsSKF&oY3?5t z_5UBqjY}84Ut?jlh~=Sa!xoE%(9%?iddI6o%|7BXS45fGS%2M;Ia$EN#g3>G3nwD< znMT3(4#tz)y(;NmZcy8XR;s5i3zmk#i$^goDv2PM*5Q#uQ`V6-0&EyaTg#vkMT&Sw zVv$z6M=Bxat5jUmRBL~pl(%ps3+scgXULD9&QY!7z?$AM>B&-q-@}}L>^;f5ltqeu zOB+!BS}r5EJ=_*n4#yrV16?lD5n^CGBMc#oAi509-TO@>r7Yig&3rfJ);oH>V!wDaZOp@so zX-!K^qt(#9(c7L2+`<7CJHBvC`K(6&yKY&tvV8o|;L?c89rdCX_`SBc)hDVQFKci& z)$tNqd)SieXsYP52I;~OIk}ez6wHRRaC`<+)r0}M1;PvZC^xtri5$W?rCz@@ZUBf{JvC+15l6g=?2Y5yTB1=I(44cqCx0lzXj$=7alqSTB`^ zNP}dsb~+1_wPj$GvF6SXV5oYdp+fp};*%ivzL-O&uv?)kMOK>WN5jj|{ib`YcD!EO z_Po6n1U_3DN_FZS&XgJsxA87Ec6u#2#(*vjuEjTRb_2Fy1Flpi1>OOhA6*_-N(-RS z_~&mquXoWApg%4|*H#YQ*1WnNHQ-{v<;xtsSAT9tuAtZG7#C>IlrMr_m%r& zipv$xCW9^2UN;lqBNMtBA@LV~wViO0Q%N=FF9sSX;_hFPIX^slL@9Sx0T$s}Q-lc` z>r~~Ur1V$S7A$?c<3y1L*H}`6UFUkBRuJ5-RB|b|K2j)Win%lyp$-xn)FIy}&1VYo z8}b-MRwS_8DheWLpsuMUiNA9qg=4?9_c|E(@k7yjo%qTqJtM>9bUeFTISyM^L#1-@ zPym!)0ldtjYn)G*SYI$2`c1fSjRFT8NWTT6RlthJ0$J9ZV~u1C>2@?k%Izbcgl5u#4T| zuni{{Jj}`ca%6e{^#)w}i12v3z5Vm2iWm*JPu!6$(>yzM1H1Wt&FOUbFox^IcWunH zi_PRksq%muy6L}E-SJVVSa{4yG63e6)F{E)+!^J@;=59sBd1rL*Y3QPHQGIxt|&}3Q`pbF){?8B#lO~XY?d>)nR;S zRx`Ww{x7s%`B&-W7?jS8RPuBF=O_Fk@F8fQAwLI~gi?D96orpyAu2D+RZE4fa8iFLwa3_>@l+4ZgCo4@Dvqk?LcU`(ILFJ3C^*j9eaiePa%N9JuEpwQV z0lS}H0xaQgy)H`2nn>$%=brL9CuX(}h(kfiGQH%GbJ_2X1_|>QL{U}HRJc9cI=|H% zIoG*JIkG#S)fXGR)qOE%4~>K|MiyS!{)Lq#cA(z3%<3f;YcBbt?w&)uIuf6;JYKk# zbF7V*V`g#y2PKlo)Th(z#tK=oN2pvZrDKW`iR?{U4|8d~?JGJq8L@XB#jPHzAXm_Xkf z<7VgbQ%1=$)G~hma=+Z!8Fs|*Tz#5|%eeV8b2vMnNn;vcRStb%R2OvrEt;P46!3X` z4$}MC1K?8BahH-P;6}lp_b2zpN`H@iGMyME$FElWSOvVE*56%4hKEeyf~Ly=PMfCZ?tc z2!(M6-58m!U(jVY2k;`C3eg2!P22XLsimEGigXlxX$f5n?m>#d@ZGx& zn2OE;nf!Huei`C_DT8Qr+T?+WPfc5S;NGCpAHjRT_aLn)#sx$PM2;TBh!^HTw zADy~c__oPe3Iu+CCa_&13Pn?DOS0@b_e5c@d2lpFAoT=^r#X*0mp0ms{OltsN_$wY zzlE&%cLCIU+2`CX90Z;GJz?{lbTOE$5Y?EonvX0rbLcBtaB#j%^?BOW?Ct?NeoPlQ z-``kv1-THr5mjnbTbP&x>oiN?g}j@MHWY1GH$Hd8`kqRHhd3QQmzeH=$C6;$c&;5i zjqkb>Ps1<^quYKw*&N?m*T;SfzR_)Uzv{#7#cT3^yz_l1cTeS4qGV<+TXAHrckX=u z*dL9*{&k;Acop7=0;nS5&#}w``zr7qn4xHQMb!`C8RO-rkWI0bSeE_+UFdp&)0kee z@1B1FBz&qKxC{#z<4D}a7BKK9A6lb_2|=dhQu-BvD~=i!$XWNF_1(jI+F>qHpI^3%{~jMu+$}Pyt_DCXGJEi(@WBxZ)Ra;DIg&&?i|`HWf;J)Dmh`CZ9dh;N9Q?4PUnYo%PH8`XG{K|%k`^kac(&@=d)G6jb4Va zr!!5a{z&w;P7d28z>mjO)2V+6wwKS#P;qrChvTY~q=@7YE%s|eotci7$LROmaW)-U zs9k%}S@&0xlk%5>S}`1QF^tA0b_kk=kNi$VX{aqVDJp>Who>O$LWNtL;0win#^k?a z<&M)i-0u~@fdNvzn<&MfeKXWdR?U>oebp;n z05B`ZgkC*KjRLn%7Mp^g%gm*2n=4@&qN>0b`=8-bL$$ zi%_L8n6~iW@}AzpaTi^A(kdYqH4{*;G3ZkYYd4ImrUq*?j@9V<{BbyxJFl zII~#Ger&XTN3NcTlkrF@w&AX?W#-_K@nbLI$idVkT%{?Bo)-UAE|1`$$0pB_oyaX& z&WIPC^xiBlbI%Ef1Qm)Aw&)u5i2pa(fsJxL%zGk`u3Ur6>FU1n!(J#bIgT4Y|E&OU zNO=2`PTK8aRo77;c+P1sRddhg`yMrR?Qghr_J=%PKLu0|>~O7Vfcks>po{CI_pnOu zxp!>IR95xCq@D9~fiX7&^yD^*Un&rFDDVJ^KDoX~^EaLuwejaZf12}JLh9nn1J>$x zc>MAnw;YFoBhZ{AdOG}`tJe$meiuX^6U7}^2zcA$h35ILQIx{>njY}!g8q}ie?7qA3Q69Rdc)+zrnrb zhrAU#KN+nwxXoQQP5n-wA*DeQPDQ|Z-~al*y#TJt2W3hiwMaV3KBPY8f@JpylNVeM z@&*Saa##bc**;7+ifgTb*d<5fpX%7y#auJJsn`W#^_H3=ld;@gnKKTL)A>Wvisizv zQF#^4B6%=ex)@(_dOY=bi+GdC?4{7CJ#l;O#FR37#p1^I;o)*$r%I%@Y^m(5KPqaE{Zy*>bd_8tLg_G>K-Klg1QG!$wqr1vR^{yaZJ07s_y zY`2Lt!Fx7pZp6UT99Mj5Vvm#cQ*-T`U&J4Qm;R5uc3wto;kEoRm~s3;Gxg}M(^M`f zEKGl^n|+^0$5x!SvhCKIf2${l-wfR52rz3q^?E7pi5lX~gB%TBLxW1aYnS%dGy}48 zt6p(DC%jMt$l`flBSVA*z0a0< z!#5+?E{_J?I}TlU)no9xo(l1>ikD7~sdtbA*tc})?H66ir;Z&NxgTdd5~*rjuj98| z;6z1|_18MB7FTX2zui~QbIK{gh)Vf$-lvl#aI8G8 z2`_5n*_B}57nN1S^yL}B=f7KVb-;b|1KGnx+0XEF2 z1G;P&IXB%2t-zloU-$Ce5ta!S%kO+N^ODZ{Ebt*T70^5^W?|?N)mvH$(#YpA=WidG zYJ7G^AcXEj6iSxIl-Sh^M~-kh>ASm^4De|KH|6KSI@`SO_{~-v~@7xW-g$R(n^wt!3SRv7L3l%3GcCP z!&`-}r%4}fGx05_!dPVm!&?T2?h6&UyfSs_zgNLq-=5CmeIM-K_9ju29|_LVkFThY%<+5UUBFm(}i@Bc5n&p=_mSutlUvSCeRVB&NE$RZMn6a)wvQ>N6J1?eCi9fYU8Q{K4 zGgC%Zp_^3EZ>2~>nXiIpA7L7LvT(eKs(~$5NU9R3nlp}6QW7M>#&bc_fTk)V@H#P8 z=WbZ>^ckyrtbF?*fQ`hjqyJK2d+s^c>jFX4oM3u~?v}T5prA=0Xu25aGCZp8QoH56 zbUu7?1^|NMd{{t*n4dHLkJY|M4c*)%aj@p-{0>?k8$Fajfd5+-(Uj+IbotT)zQg+v z@6$*d6T>uUb_|@+zPR8 z$Uo=%XEx3CGz|D5+@|+|GnDE)Y|Ok1g>xgcG!dscqLqbq-2t2`G)Y{2&oM%jMdtBR z8Y`Z*674Jfw(yvtOwf8-XhW1LbQfC!gHb~?GOfdeZ+0N{b^k~EEYGtMpQRn~w`;H3 zJ!tSp6n9)B@kd$EB5>dEnL#!UedvsSm0gN4zh^jwlS2wUYF#L0{=b5M7q;j@uaBry2zZCnQDI9d<}ZMdOz(}Kzsf~ z=*vHr3ecXYf)$914pu+!&`oD>0wWW2-m=4*U}HsnPT%PZ|4BVBe=;d_G+-$MZJI`4C;k5Czw=ATW9kA-+xgnnB^kgi_ zVAq_(-n~Q4e9Lu!x+FQvtW%ES#4z0(VwZV{`Y)h=sDJWx0V;sqXQG!y5HsQVAxn_p zJ~3#C*m*R)C%yY81b^HvWWX6y%oeZqC#9Z$c}xBOX1tlLM(%TxVyeX-Hwr!Iz&NM% zbXlt^I@)n$nb!L8X*!EK9g&Np=GAR}^NXY641B3989YW zHd<{(j4!0ec70)P{6)#|3zy)GvhHq$#Gu*W)iRN=9KsE%1#fsp_ zzH)B%(e}1|2J}-{e)acF|Kl~%u zi|1J%*gs)dem(#da0b@()S4$2cffTegnO$uw7puQRT=p-rXdoNr_}44Fn`mSEMh5* z3%qIXlhpWXF+IAu0#EAfElQe)QYhrw;4u_Bymz~-SkAVbM22Mb)fzan-rm4kp0EB2NL`8t@Q_&paJH% zH9CSK=~>hdnX z$1+aH?rtiVU`QGKrO_d5az_N7~eay0oSGCON zuMq#N_`W};IXgBde*Xtgn^lXuDRa+(7}!YkZbS4sy!A~B@KF(M7^TqnS$`&>6w>{y zH+;$9fb!lL1lgFF9rs9%w^$tCF$_r=yoGR1FV~9y-T#>IJ%=;gJ~jOK^6j!CSCVfR zdGjIFqwZnY54-+q9|HK6gzF6n^c^P&oB8pxhZt$-7UvrH2I+j*qs?*{Z@}T1OW*x2 z&#~T_pSw)|GSaRBUEK4V$Q7IyKC#BJJueV|JcNwJPc$nasq3l{e7XN#=x-KBOW`rj zC)B8o#?C&9s|E_ZM*p=)>!nRLDs84Lyh=5+2a}&Sj?^S4mC`#B+-JG>T@oRZC8~f1 z9cpz_rPgC)HL8bBEvL@*6(y<Q>#xL1kHXM#kP)5v_9_VZ^eK#!)xhDfm=WbniQ?qBrKE2 z#ZvHRKK;BNzZGekU^?da4K58a+ZFM52=wls%izJgKO1aMVlA-nxrJ_{C9jL9C@{oc z$oD6^-T=2F=U=oinGU5Jr!D{k-d|~gkIgPCJL1Y$4<6mI+1Z(qrw?gA8HW4mTt3k` zXqUkOMGr01hokyG>T+)K;cAB0^86&BeOhn~;PI-9J$2j^-emO!TW;+IJbe7T;>-fF zbn8|Qh{Qc}n_8o`tgivC!gH4@5OA-79mc#NfdzFgAAcPmhrGObH}6_Fm9m-VhuF$K zQ=TFDrN05E3_B5}UE^m9qT|n|{>M#3s{&J_fOK8)+S5ca*G3E`7qJC&c9_sHt9gR? ziZ7OHo|&Y@X0MA>8~AYlL`fns2XC;+DI$9$QWfb& zOA6h(hj8Be32t*JQ?O6&BkE2fPf35*DI8U#5K1>@qSJ`u;mMUvCsN5kmFToOgWA45 zd`od~nj!AABI!P+-+G;|-P3BlcUf;dt_iCDl^nm3rt8+v$hj@<0SQF)ccomsh(2H= za;7S+`nMg(g~PXkJM+uPUxLpCAgKRn9`)0(`1)#Y(rDtw1?R~5ZI;?))Zfd*dJ$Bi zE65ML&S`xkB!B&y7jSQDxZh~_x(9j8(mi{>>5}LCqniOoQ&He_p^D$vaLB#!$JlSq z0ITmc4~j>Oqrv>V7Y1mWCI+~60>C#6%3i3rQ0W@~Ai;yQ!MEOySI@CPL4T)P&R1Rr zzCUH&7hYNKb&~I^AAf4cL^nPw>1GbS2k(RF|B|^Nt#eGVs_@Fu2Wb}-OzkDKrGzlU zpe$8Kv9iiYV@m8xY2=cNNLhj;y{9#HtA}`{7k?etP0I0-pW?-F%{m|L$++8}lZA>& z$r@p`w`j*mm8n5B6!f^uy4omd2i`zhw+bPKBEZnq(U7BU-Y^-y&-YXff3$0Dx-T)> zWxqc_I_q8c*Pdl|maE}!Obr;m#xpUF?yU2#yRTKhTDT>J$ZowN=E9lHE;XZj*Mx_6QpzVZ7g#LvY)iGDo%AkG;R zh5CCil-uB9!^U!5Vm0s?1*_rm(b;%EKIYO8p)JfG;+)ap7#f#Kv^xl(_ z*xw5zx^0)~`;ThjxaJ=mUk|W-BnbxW47xB1237_<{ObK{!vV+cGeg|vBCIztC6r67 zc`La=98>wueGuQ6A&$wv?afeT@giPIb(R_O z=NyccQ)*%#DOY_$?zCVRe;b*DM|J=_FttKqkek{{{4%BGahD}sIx}|oZP{6@$E*T8 zLql%%as0(1D)-~#Ek|zH%H?>`mCN2EcB{r!8l@$$n{G5gmo={p9<^YkxJ}2^kKUVD zV&9-Q2<|S|4a4ggpdou2{=3miha0Bm)-6j4dh}1dm-NRQR7ddBdN62zEY)S?baU{O z7kAE~m4zoyry3)S6#O(2?{wJu@oS~2ky+CMogYWe*vsiNNQ4VqD+qKv+|HlA%Xm+m zkUy2S`i5rvxRZn!9Y5^_?vz@VdTrr?!$=U61P=Zw`t|mlqYAz`)w~{m?RIi{tf;Kn zeGa&u=jK27_HW>mF3p0vXd!y<3+GL1_F0Ydr}sgW>JX@B>yxitcLza#U!L_je~z#E zVUja>UQ}{}*Gub6feKk}V$K8TlE^5-Q}LBvqSh{-j{&~vG16+GY2M2Epa*#VtCKAo z&l425zr5Tz4|qT*s3TTPPEPLFp9q*Z{n))9pPV#_p^~Pfa1MYDbT6GC+>dE^X?@j4CMI0_+P&7^sI_+Sng`n23ef4# znRJqzs+s#!?hI2T7C{E7LW%Vf5)uLf1yfc~z{H3u_1UnzI$P0H07Ilh2dQqYrYuz~ zghgpzE|rn5uuJ8hvCFm8m2=T~xXE_W_-Jq{cvI&9SX=rtV7>5Um3`BSK*Mw-)MD>oKk8E<7M!0uACQ{cPpGNJ<-)UNQ3cH9c@ zWN(2y_k7&FuJT`(+xF1xxw+Zx967B@!-th3<+<{a(i47!18wcjg(Ms9vMNh_uL9m4 z+i{Lpqo;1JnL5FlKYfn^-cR%e4_k@A%b7eH&!!z<+<>TwN(F*&-j5hrT&BkM_E#CW zxo{%SCv4y;=j*rX@bdX@cM~sdpjKiqyx>bRIC%f~=11s!8q<1z@ok!+OFfSP(Y2}I zgc}ZUyScnJTpgNz2S{lyX@ASeE?Y3{-Zgf0$UhZ$icV-^DzN{h*obH~~)M^ec z(8sl3eA}=2>-`?RX%+B?^XS*Z%iovr+wP(6S4D8ty@i`5>Fd618h5MD$M5I`r)00a zn)mAy4=pH5libR4YjIkHOKcupcPd>i>Aeop{lK{}iu9@;I#SQ@co7ZQbJmvNmLH=3 z#J?8-t_3he3Q+kokFH5gnoIlLnbv&qS;SzzCk7|aa50&W5?1;9b=yQ4qC-+H>=zr8J%`X}F8yq5$#-nfq3G6{Hi z=yeJZU#L6Z{|H}KhnduOzV{ybb{^lJ6NqCfwpSWodo-oBv4vQ@TkaQH?6u8tbf+egC)n%*0$_T8Yxk>tJJJ5#)U`h9n>dl@w(vnTZ3|0PAHtMA7_s|fKleQh zWTTP`+70aaJ?3;z@Z(-suF75O zPkoHY3*3)v?U?31O|K1IR}cM`W~rNna{ZN9Hr^@^g3Sgj@UkbpcSa`Sa5HwXZb zx3`ijst;BbE%ZdAz~&${LzYHzQJwj2bi!U^mC;tBnp)~`UcU|#MI#fVMV}H|GQTaJ zIDvkYYU2~E@4hTOq=`<9m7MigOJ0j9O$`5K?@gGysx6K9*+=kwbg<%)U+}Rv*P>?4 zanxDQU|Un*JsFQ>zQ!CZajcfdpGIQ$MQ1BVw1c!o>L!Z=CxYBX$-t)x8 zaQ9kKaL_DKJ1c(tJJP*t1bysF80aLLGxsxz4#<%^^QKU(Ap*l z#rd>N?SF6@S39`iKcui{4|>u7jXur)TGZnbERH7>@To#?U^2rOX#J*o^x9WYa~!W( zwV1QZ2qSuft?g41oiS2XP{TW!otY#{QS!BUjH^%y0cH$I()J;4>5kOc>5jIb*`oQo z6wVn&%OO$&6#CfyEn9`jvfxTTzWCa_+$rK{f_zT@6^^>6ZQLXHC}3wsl6ZgdgOMsZ zRq*2|*Ua|1LTpAcAVvO-9T#_NGp@0LsL<54BfbA=+2tY!C#Ce(HYsXZ&ng>sBt*%I zpp3qM!ZzJugUueh1<}s&$KMjmM-zFrMl;r$n-tf;#Yht<9qSY>4FfJNz1!KDjF4b0 zCmkB?VX}dwvWnDu`Q>hJvF;!nV=X(T@{{}s6`7KWAF8Ga4Wub@5u$5|OpX)R%chqH zGag;ennSis2K#9OH}B&W`>mazsw&_S9--p_GmE5*wsMqG6PjvY6xox|48AEzNk|g6 z;rqMl0WbF9<-+hYz?td-JfrVhOe9*w?Q6_e=hUH zmIXToL5d$#ih8fwYBgw)jz~al?f4lMp3QM%ny?gBvd}6--o`J)JaqbK(h(RhUE3x6 zTh0MrGLK|PJwzAj%BR!B&XIL!MdTtjjTrYFdR21;O%|w=f~!~;;oH6xBMoVld?`XN zpt~cA5<6I9i!Q@Zd)=-X?V7nnJ!W0>HN({voIVI8vfi zw@U!p(X~-$jYU(}m^e;UjBDu34wLgGJ;Yq^zg5}Ajn zAq13Cw`?*l>oTxjS5sEYVluj?<8Nn|@FA`C!RuC8Wx%OmBKMlHts;o4jFm~0V7_pV^bX)1{^fID) z-l>nq{v?T5u@vc!n5q5lb{MGAu*~bZk{YjE${lu8NuJFHMHst6qL6T2VP;E7&0{E+ z{_ATi>T;*0guX5XHXoscsA*^(*@~GQMoje6A7yu|Ib|7>`Z8|53(5a)?RDd(FP26i z@?97VJm^J4^=c>W`c1KyEM=S&Gf*KNI{P?=TM&nMP!jBW%dy z9MQy>V{OWr`>{FN?06`ni?YnIM%WY!%{AwiD^HXnN0BsVt{hJ(A#yyao?rD-J^cmW z*XR3vzu%wt_w#zaKiYLIA*(=kHxb~fGx@QFaRekVMc)m}r5)=R+Ua##K{WkLr%z2@ zsMSq;+%zUp-4>frL6de9_b9{_u2>L@x9Sdw&WlSPqnvwfit~_tx_Z5Adw*_*qH#h7 zUm3CJ3}+ZCwrs*$!sBm7&f?RGf*aHeYl0$Ny^l7=>R`(zO7c=FP_t1znR1Zx&x9nl zCPtljxgZpKK1H;%uUAZL?1RhE$QPtGT8@t~>XnL-m>1ne!>LoM7BzV-B@;5vwy}BIlEoJ$Rcl+t}vYu?wQRRa~xTmI1LKYBX3x2ol5QKf->p*qbTXpAraTREj-|I;Kh~{RsE;$p4b`(fk@R;yP@Qu( zU91#N9b+Xpv?5s5TKyZAfweZY+Qz?UQ+4NL(cnir58*~(m^N{W?Tgw}CWib|&?fLS8qnCGvL}mB z^kqD+DaaYF0bUrNi8~-b`2#TBiADhlol;%9zP$7>^eOB2(~_bKZB2|NFe(ZUl_t_Z z^(plzE2Q3Dfgw1j`IU-wh%-CS$~m{<%rtXnw2FegcF1siZpr+f=V*9j>3!Sj(X=0z z+}v*9r-4RKJF)k0{0`BISNMntl!AsCJBNsMcSHG1lv1-|JzyrL#{UB@YTJEaXLF&Mt^30K*}ujVh}tsWsa3$$#kwHyj}JAq}5 z+es&!YsFSfp-Fa(1o@C(0b#;?>gvyW?eA~?Mt31S?uVf@V3-7X%|rG|%xr8auU5Gfa*aw?{0ym^V(e!N z)HuL4CJN!&T2e(x1-&fRn}UEWce_-_RL%M>uiSq~{7|PEZdS5yY)|MUR;ul#4!{`X z;P$k&fk85G({_g>Mft%gS#Lgs-+XsILQ1v^;2rp^zgczsUNhEpdzjXJ1NaN@8|b{r ze@gag>f#cHeb%yaLq!O7ZcE0_?K))tJ{M(2Myu+JkWi_7JI{_`nd3+ep*$FLd0wql zW<;4cKJc$B+>V7BLAAi<_gVd(ctE+^Opz$-E8zjI7{*eP26n|t^ux>$WVzj&pq+H8 z@BPDbe}7RM6#Y~&YI6Miv1cOi4h^(K+Qy!S3VBs2crnI~j5Dy`UR)3wW7wMk$vvie z3YwuusAQ0UIcyP7w0$C+1!Wy9PWs!{0$>4{0FE*$g2@6>1bVOO$Z$tVB%*%Gp(srq zz;sk(X0hf-4h@dcj~d3pw+8AC-<$Nj<%C?^a$9G%prA0iqZ#|45i)6Hpd-Ug2LoGXk@_w`duX>8i?9 zz&E8zNSu@fT$#TFW4_voSfVsJj}>xzb*n9qHZceC-v`0+zZJ4(A$t3{cTtU{&fEG+JjFc;f6c!V_YgXaNrH{V#5<2eP3Agp z8L0d~7d6>tGxB_?zkX12%tJyI-s;w!FO#fDUP?t+y~G732p0zS)#Z-x6-GV$TGVyR zUnI`ReD#bvQ~#!GU7r@PICinSScp1i5_FQQ#AT4kt8t-<>R|g_Oh>RU(C_)Hfns*p zoM{JdP!(`Z_Rv-;-axTFyiIT#usg!3o$sp1ax`dd?0U|P=hr9i#9#7OAuH6Ap8Qhp z_N&xCn!c_{*H?tl?+C1ynxa=BRq@PxNuIaZ3Sa&5^Od8t!}^ewvlLS1O!+?hKr+FV zXkPjPSxUqqEdtIQFBBBWudsnx9nk~gSR}f&jA6{nlj-wrkKV0*OSYSA1SmGQ35z)C zT!u;kg|%AZuB8m9EGD|t2mgnH&k3Yo_)3ufpoF({hhpZEiqZpW8QXc6yIP@?g|9Dn6b9Oc;r9*1O ziU5QOSDhsF(?fS+_*Zg!!Xl7!(M?^JXzSAlQY~e=l*YS$Qhtj8-|qG(77GlnB7FSR z&QatBwFoZqW?eu|zQcx;|bvH@$he*w#dr|n99m}59kk7m`AU1kWVj+ zC~(WRWkIj8F7HK*0$DO<>q$%h6D#*x{%WaWwNiiXaBm;o9zot^dD1eLlu+1)fWptH zf{dF!UmN70b+n?X}|Vg zr(=?;U7I2w?0~-gcXoY#Efj|#b0zZ){`fSYhKJ^2%{Wapqx8HZ7c)N%ozj8~&@8y| zIK89II33Bf9yzFcF&mdLuU+BpjKBI3iJ=nn<|aOuz9zLoS)snc`VIZm1!%0P>E9O~4V zZdY0(b$%|lF9kkt;0ClurQLwy7qK^yS#57n(g5zm+r0n2XF2Y`maCS<^UaB-Jb9C@ z2rgrZ{9*HdVx@;9iuU2pkCyQ#gWOW*GM)a)rNn-OxZIF|s0n?NWs`qPh}T}ilKTP! z-mj&p!isCMFG-eacA-}JLy?T&hSxK}X^p)5S0^ugy3`)>v_vt+&~o{FwCiAN-Qwf3 mXIhgVzn_WxLH*azZ+mgl)cbW5%h9cUTiaaFt_~tpFysHdlqgmJ literal 0 HcmV?d00001 diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6585a85fa84327fa3001f2859547cadf393086e3 GIT binary patch literal 4286 zcmc&&OKcle6n*pYZzi6n3M8ZyAt50MA5|4j z`Lm$u2M|JRun=N_V1rmzmAXO2fLbBS0#H}DZE)hoO&a6Gm;0QIHE|ke#ztYH>v!kg zbI-Z&Jgu%bQK0-gqmkJ>!A>>Xp43T|kG+sY2cV1etl|akSS1_x#M0iVT2%GQ* zU4uHx5a9+!_O6NgJK4L+@v^_NI(nF8<7JN9w!pIH`xs$;aBwV%O>x}zd6uzmVqMxt zZ3BwE&)QlZgwW#^hS7e(M7)vQ*oiZYdRYiPQL(k}fgxgXW9ViX>2po`7!t;1pd-uZ zvsiJUMb!a{(WFo7HEQ$&AVkWwz;Bisiv-7Js6I7II}ZVVxXcp$Gb{|bibEL71^Bzm z6yx9{!O7?LpO34|3OW)DK@ny|(Uqu09X) zEMGmmORQ^CeXl(zUGHKKp#|hR=;HJ0G66m^B?g1b<9niOwEgs8C|}|CK)ywY_>Na$ z#EC+%>*u5#sZC_$+iE*KJhN{M?i=}|rH$Nm0YqV_!gKOD%DqV+{Yq3W@9q=e*aJ#S z?ctw!YSB>inV722nwoLL)?;&V!z?EZ z^Jdbt=6ft_q1Uo+9eVgs)!}L$4?p_UKdDr@)Z4Rjw!3>rF%iF`XxshMmbvYEES9>a z>pfRBEm2Sv`>$Y7yoCDW?sf3uD@y!&u~=-DhC-nLl-mnOCm10tzjgZbN9*)YoH$cM z-d8)}_E@07^m#*$ z`k?3(IxytggTa-xALx|SPd)CzG`$Y!+M`dObz6EkdvNk3ea9xpwp4sYtbQE~4&J+n zr(3%A&RkVBF^%_}Enj;_pXY@)c|QEQbN)Y!^!=)%cdR9V%u=?NSxPNtYH}%C3th?9 zn8_^OOtbjKf`&~p0Msx>bGYdl{Ba{+!I!Lagm9De-wb#_Nrr|krT`pU$^bZ4%L1(0 EzZ5<|NB{r; literal 0 HcmV?d00001 diff --git a/static/flags/ad.png b/static/flags/ad.png new file mode 100755 index 0000000000000000000000000000000000000000..625ca84f9ec596848d4b967b5556fda897ca7183 GIT binary patch literal 643 zcmV-}0(||6P)S}{rU-#^xW|9%5S{`_G8k=zVG=|5luWB>#JF#yj01oZa& zTu&JQ008~}|NQ&{0055x0sR95`v3p?1_t~7{`v!N`v3d-{`&g=`T6amqXIBA$0h)T zKokWRkpoMx|Go@O4FvhR>O1p+i7`B6t^3)y2dJ<#?4I?d4x-E}Az98Z`2`TmzkmP! z{`(J9{pauh-+zJD{JOYLhW+QSzkh#x`Y&wke*WLjpZ~u9`2XuCr@{^OJ|MkZb_UGR@Z=V>fB*iaq<8P{*B}2u-uMnu#J~U$KrBDL0bT$1_irEqiZcM6WEF7g<|*+% zzkjHi-TwRc=f7_t9|K*?`1?07lmG&V<)V$@{}>pUYZ(~+fDw@M3(WY1!T<=sp}f-o0K!lJ3<>Jc!&VtMslE3ph2^H3FGe(F z4|^}slF1@l1Nxc}^5hjjU=0la|37)k@b@neJ^1|l@87?_{{z(l6@kcKe}Db@_Y267 z<&g#201!YdV6*>2R0EZ*bYA%%2Vi7me5m>mAb?naPGtnD20MY__n-f`0mzg8{s9CK z*hzmtW)s%H$oS{aAAkS?JLxaT`2W9u;n%DD1Q-B%#%TuGpFSS|0000&5HW|5#c7{r(N){s9YCi84$~U|{$Glm>AD0*K|^yLU}ZO+eP) ze;~;4`#0mCzs$dX{r~-&?f370zkUTM{{OB1_1E8DKmYz*JbN)f0I>i8Q1#1Kum1f0 z{pZggpt}FRe*ORX>;JEx|9}4c_w(m__W!pfKK=atLxM{ZXbC_7vHbh@@9*EgK-ECy zzkh)Y{?EYhFEQc&-#`EN9{7Lq7*OE*@9)2U|NQmmC(vO40VtezBLF}U2%?OI(TG)& zJC>!~;SYmN-)zb6gc(7A-^Q(#z0vHb9!CJ#T#ii{@&pjek8j`pfX)5|H00+GpnLvt za{MtfWBC4qk%R63j~{=2{QLgr`wyTb*amx2hce{;{QL3a|BhWh#6A5bKmf5of*%NgdVm^$Zuo!gI_LQd|G=96g51yWl0Agf71rLE? q3XWqq@&Lw?Iyk0*94-cc00RK~MQKxxUU$F%0000= z`}|7!`YQYT5C8xIh~*yx1DgQ1_jkSr_QgN{Gn)!7=zkRT=l(yS{y%^Jd$>%yrP!)vVq~xpZ~xA{{HjZ>hB|)Z~OQoBLBm{yCqA0{k;C`&+mV~|1$jj15^wU zKukakbkF}kzkmk({q_6zmUTZaUWBOraqa5!?(V-pgMR}R{04?0&`AIR#PSE^hrdi> z;*7t4gLVFSJ$J5DTibsorq@%ai5h7B2l^1G@*gM=fUW@uAjYc<48V{DX@ijvE(11_ dg@FMezyQzuBHsGv+i3s*002ovPDHLkV1jTADB=JB literal 0 HcmV?d00001 diff --git a/static/flags/ai.png b/static/flags/ai.png new file mode 100755 index 0000000000000000000000000000000000000000..74ed29d92616c86757d3c0ec04378301c8f591b4 GIT binary patch literal 643 zcmV-}0(||6P)ocPy#thD++(+#W%SS0Qvp^`1%0+_yGC)0QUL-|M&m^ z`T_tk0M7pe&iDc7fh@K74M*k&0{8f*88BG;;GY6ve?;&4{{Q>_{{8&^`u+g@|Ns5} z|C?Nv0*Gb#mu+n4z6ZW~#qjy_|6ji&ZeIEMk16oyzH)i@Pk$MNxLARR?a#kwpMS72 zF)}cG2M8dRUN*~81wCz%$A2$f`_IL|A|w6j^RHtc7{2`lYG-KkQ{(*knSqh%3kzr7 zs%s1kKL7#%F#yj00-^B^D;pD^^AqC)4C(?1m>@q26ceiU4*2{4`u_d~@vQ*ykp&A1 z3h$o${r~^~`vQo?Pm-bY@cm79zVQG5%l!B6)Riap%L`bGemnT<@2|iAfl64!%YWS~ z`p0YY_dig_cYpw5WYAmG*lPLk!;jy8|NZ#~MF0Q%{rCG1ko^Di`(Fhkd3F{?py0c9Vg{{gA~^#>F<|Nj5KapBR^@Bf~CXSi|t;lF=C z8-4)<5DUX^V9+rDCI3RuUy#}V{s46X!}R~}@4ug&fA|+@;n!b(1zCZ900KKMX({{sIILBZE*UQbc|SngJt$+`m9Kz!<;5 dBm+Qz0RWL%NC2%sdjkLf002ovPDHLkV1lARHI@JX literal 0 HcmV?d00001 diff --git a/static/flags/al.png b/static/flags/al.png new file mode 100755 index 0000000000000000000000000000000000000000..92354cb6e257be2cade71cb825027ce8d9efc06d GIT binary patch literal 600 zcmV-e0;m0nP)PbXFRCwBA zyv)hK@{fUmi2(>${xJOc&A`II@PmN?BKC`c;SU)710$d~Kmf4-r6niy16BY3`}g0! ze=L9h&Uybegp-Ns@87v^zGQMU|NZla@z1Y+zkdVKmFbHB0*HkH4F0@)&iwE1;&GRLuzkX(OGXMMa``@o$|Ni_2x(y(Jn3#TnHT?el|IhC;U%pHK`}6Df z@2@|9J^lXU=Z~KvfBzi*^!4A*U;lv`e*Fge3Lt=(82$q_`~m9z|ND;$!(WEqze~6o ze}X|JFO$H(U+VvX#{c{avi3L7@c;n?3L&6l7=Hix`}emN<6i~#|7+iUWBL8-&##}m zKKx5zXJr5LA80nvkwCM6egFs{po9Y?& zL6B;olm7ns#=rm&Kp-3b{RIXY(1{??Ha=$NKfkS+|9@us$Mfg+93j@fzkY#y2y!;i z&kO(o1PWH5>zPHwn7|>;0Mz;WPl13iP!Y(WKYy4&4hBXMSPugOKmaiw2gk!dq!|4J mj$lZE0R~eis zC&uN{cDQ8`!@ZNLOqwtoG6nhzPx}SV-d6yzFfcIz75{(rngJyDkKrFw8iaoThHCr$ zo8k9wMhS^O3=9AP!~%5B|9}4(ASQtkLN$m1CV&5705X362095KfLKJhW;OjWdGY7l zzrX*$DuLu*pcFC#NdEr)SL!$SY=)lz0mSm_*B_u)|AM4JP)(}33zy1Lc&_o7? zKL7y)bkd){fBym<_v`mxpbeRs`+WRVot-5A{r$IX$CJFg13u{3eX_{0R++j zR}GCkSy{&4zk!krKoTg(2n-k?g31B}5JW!NMq0d0000q< zM7LZb@rX?|r)2RP-+aO3(7q?c3+$-Vv0E)PxM3SDV%@s`#GkZvw_x^WBa~uq3^l}t zWdf(j(=(>^SgETc#5#EZT(4ObRkfxbzP9G;yza0;Ygc8-_*?EP(ca#`l6-Z6D0{tL zQ4|~MCSi!9Q9YkW=V$ix#EqZ!rc?eWA0TwdVID+3aqrYUXUhCI)Ad5!(cP!BhhW$Ayb2&r8kK!bz*2`~UE W(Ndrek0Jg50000NL7TW{{R0! zJP>i}(MpzaSkj z=eqvqa`^|6{{QbQRQ11q3~Vg_kG}Z}5I`(hw}ncX8D4$|h64jUK>or*=^r$dfI%n0 z$3MKz;!F0w91GjkkV%_X`@4 wKR^hm`X5LwD9*v?4;TUC6Bz$&3;+QJ0QLcJ=WT{TU;qFB07*qoM6N<$f>3bX%K!iX literal 0 HcmV?d00001 diff --git a/static/flags/as.png b/static/flags/as.png new file mode 100755 index 0000000000000000000000000000000000000000..32f30e4ce4eedd22d4f09c4f3a46c52dd064f113 GIT binary patch literal 647 zcmV;20(kw2P);Rmc0RI30 z|Nj5|{Q>>`0R8>{{QLs@`vM01^D6+%Ap+Y10Qw3Sr3ED)s=c}b05Jg0{{#dC0RR60 z)z$y}`~dv>0Q&p?0Q>ne0Khr|#}EMN1`-ttJvaFq6VTDg`OMJS*4F}vg#m0fQ1$QM z42u6QTCg3Kt8+Li1J#Y_zUd>4Lw{URe1tEjLbKmY(S0M7pd(g2qsJ0|z;{R`~d z`n<6E*3bb41p*Zr{{R344LkeqyaMyV01O!W0PZ0+TjlEQ0st`p&i?@b0Q{Gi0m{n% z`T6-B``1JQx+4kM0{rp_FfRy7P6Qq%_1|&<`|JPx`Uew8u)+%h2+ zdiAI7xt}_}e|$CmbI9$2>u h*D)|iF)#oG7yty8`q2#I8zBGy002ovPDHLkV1j5VEF}N{ literal 0 HcmV?d00001 diff --git a/static/flags/at.png b/static/flags/at.png new file mode 100755 index 0000000000000000000000000000000000000000..0f15f34f2883c4b4360fc871d7105309f1533282 GIT binary patch literal 403 zcmV;E0c`$>P)@|4`Xj5kLT%`al?B=W5I`&prl$WjwHQRjfmQ&G0jUOA z@&|Dug_Rm`2Y251|$~)1M2@@6mI}!8O6olw6y^Q z5X--d7nzS8`+x5q12kBmVFD!~j6c5_fMKno0(1^Q0I>i=is|LjXX40RRttS6cG0UZ?;7002ovPDHLkV1fxUnjZiF literal 0 HcmV?d00001 diff --git a/static/flags/au.png b/static/flags/au.png new file mode 100755 index 0000000000000000000000000000000000000000..a01389a745d51e16b01a9dc0a707572564a17625 GIT binary patch literal 673 zcmV;S0$%-zP)>fJ3En$GhGS>sbE%%m3$AD)q?8M9y>88-}kR7#RKlk!P~Y_PLuF7~U~3`~nC7 zF#yj00ZUDdpLsm{7ajP|&HwoK0Usg|6%f4L_{`Mi{rvv-`ukf=Ed&Gs-sA7L!Q7*a zj{*QO0M7pb%?Sw^g@yy{>ihEY{`vU@3=8@G0rvO$i3mOL`~mv-`W+b$Mmr&io5dg< z5v!7q0*L95jt`TzK8Kd(Utv)OSp_aLv){6ccV+Z`{Q2+asKUU&aO3`Kpz6wW8wp`< z28M3{0mSqnB#A*-c*8%1=RD#sSOwMznKA3=e&iEzwo{cA=PgXK`2OQ}gqId83!|%* zA_Kz@fB*n70M7pdECCwp4H&@R`1|(w-}M5x*74i)0}%fAt;XafA{48))#>Z>?CD#}*e}Ret0tl$#*RMZ7}Jl7Z|M45`5*URzH9L z{rmSnPy;{!u>dsyO%meg+00000NkvXX Hu0mjfN{&}S literal 0 HcmV?d00001 diff --git a/static/flags/aw.png b/static/flags/aw.png new file mode 100755 index 0000000000000000000000000000000000000000..a3579c2d621069c8128d7cf16440d5e45a3ab3cd GIT binary patch literal 524 zcmV+n0`vWeP)^}0h@W9fA9JOn#opKkr#VO<{LPm{QLg}EdH3Vb1Zzpp zqiZ+XNBm%5{`ViKi{bCTe}Dh~2a+HyfB%6Q|9}7eKVjxEfB<6Q<6r}-{{7;G=hB{; zLB+p+|Ni^;|DXT={s75;AoBO$f4@PDf8sps4FCQC1Q64wUw^h*hy4HZXO3U)@85qw zD*pci8U|DWQu_BVi2MUWKoOv;00M{w=;|*lY_o%kegGx^{`&*A{SSok_usF-P)oof z|A86+0*K|;FQAM6fB*FtoEI&bZG5mq328#as{}<$F zWDQ{Ffz1MH001phxCBNdC_2H6U$AI~GQbEJqg*Ti0R{kQ(yH?TRrCY^ O0000^@RCwBA zRL|V|7QjV)*ul;Rlceqc6W0{`~_8AeMg& z|LUXE8U8c=`}dziv6P|mEu%JjM}pqh@65md|Ni~w->=_)fBgZXe?Nc!ot?WGAOHX{ z0M7pe`uG9>{{ZXo`u+d?{`u+u=j#9V2j%Sc^797z|N8p+{rLO+`TP9&`u!FW2mk>3 z0#G;xDFDDQ2vg|)-@5Zw?JVTrAxNs~5&|AZ;5uWfQ`vUsMGm`>aGWoI7=P|oW>%~E z|Kk8dq@aG@HzdsB>fj@shYzX-a%=q<(frSYmfLIv5{HbzQ${~M_P_y6C&m>3uU0*DFd0+8#0#Gk)^|NLQBRAyCBeB9H+Wn{=Mr@*PJ_ZKV< zB>w@GFfafF5Ks>TBP0ZV{bBt5_ve*sKkwd`OiO#eX6=u=cfX!E#rX3NBT(umNC7Yu z0Ro8S7X#4ce?Xr63$phg)Bk64X8r&5``_O`AFf^jc>p5y?+^38KVKLa00M}`wU!~x z=I_1Fe}Df4D*yZI@1I|PfBgnRptt`0`2{iqMpRNPo|?TW>MPOFW(r1_!xlT-yep5|G*@e%^SFxf#DC32@n7=0M7pe00011 zNe=7o{FIuhzQ(sjF~t7>`T+s_0sj00{`>*}`~Uy^|Ni?^Z7Tcx|FW0O0tlpm;s5{T z+df{s^E1rDZN;v4VP>0|8GkY`{`&t1s2YfV{r~st=KUYCl59Xr00M{!Xv@F<41fPH zoWA^R$>xvtF5yd$xc~X{7o-}f=ig7DY9RXc``_>1K-c{N2q2&ahQEIq{`~z1RCDs; zw*~7zIJ!sAKj8J}&!7K)enC|K{|nUc|Mwq|27mwp*#K1f8;Jh=2byv8+}GKw-@AGz z%-rMu^XK3HKtBLA{Qvdq*I%FsKn(x^1k&&qY(LP_Uw?q)|3jz0PF?=a(?4bU?qHzN z5I+E=z&88?2q2IKpz*){`~&I%+VJc5-=Dw#?LYB#!lHLRL7BZ8-NZ5x*TXaKmf6TZ20qEQj`^F;~x-W z05U+*fBxNj@_W(7kJ(ujGnWgh$g%(Z2hso#K#U9wI~hQ+`3Dk_7z`j1tL#*2FTcgW f@C+EF009O7>dRn2w6d?H00000NkvXXu0mjfueTzu literal 0 HcmV?d00001 diff --git a/static/flags/bb.png b/static/flags/bb.png new file mode 100755 index 0000000000000000000000000000000000000000..0df19c71d20d7fdc06e1cba01028983439b2bdae GIT binary patch literal 585 zcmV-P0=E5$P) zj51&U|NhG$_=oZTA7 z0mJ{l|Ns2{|Nr-Y#^3*c{=WU_!>_-;fvROCxPdkR1P~M0Xa=C_KR`47{sXG+pY!Yf zqu=)*{%)W0>;M0szkdJy_507SKYxDz`3KYh5I`&-cY{^``2$3Z|NsB`^ZWO@y}$qd z{r%_nFQBC$4Is{6hz5WF0@(mI8^j0N`~Tl>LzQ1Ye}a&q>MtPo*RQ`|OMu4x1!@2Y zAfSdne}TsT{`c$8-(P?J0c~)w`~?J`zx?#Dg*yp^z&88=2q2IKpt(TRKniTbum8-y z1bBWi0e#F0wgIH{FVJ|Pxj-8L0tgsNfByUdJMs7b-@h3^8h-!E3i%CU{QCW$1*GB6 ze~`2PfHeF82q2&aU>N+96yg32bmdP5238q{|Gydk0=52OVEN6!@<&OA7Z~7wL16>b z@DCt>7#SFn85mf=5eY;LZ#OUe_l@D-2Zn#Y82<4v{9|E|jkRQ8_`$&N0jL_N03g5s XX0T^_9W~6o00000NkvXXu0mjfXpthO literal 0 HcmV?d00001 diff --git a/static/flags/bd.png b/static/flags/bd.png new file mode 100755 index 0000000000000000000000000000000000000000..076a8bf87c0cedcce47099c6b74b59f2c9d1dbce GIT binary patch literal 504 zcmVCcfPV}OzZe+)fYC1)`hmpw%>WR9L@~Dk00x0r!@5ksR;+hV0>ZLfIcBS@orh;x zv}95za5W)x_7^}bV3YqpdH?h;NZnuSC%?V#{+0gl`~RQckJx{&l>NO~;@7XgzkdJz zEh{VwR0LSuiyWGR{aKQ z00-R5+27mwp+3@G@Um)kt zzdstU{yWl9SIB_0ft3II3)BD*KrA2|{``^Tm;4PDy`=a1tTre>fEI#8 u{{2z<2NVVaum*qtVq}m+iAaI~Aiw~?reC_kVQV=60000gsB+3J~Dn`pdxgmx19A{hgns>oXkhsL8>j&wfIu2Rsv+R_ zAB5T1GyoL?1Q1BWpFe+p|Nf1n;TJ-~Kd@?uhChFRHUI<=#0HS!U%!8$YJib28yLY( z0tg_G2B7i3e*FSj@aq@44gY_`jRywQ69xu=0D?FP=vyd&x*M*V6|CV8P{Uu4OBfgc z0tjNmA4y3`us8lPG6H?}8%zRafgb(Cp!5f*6oP{xAq} rF#LPR@IoIX4%E%Yz;K6w0U*EtuUcjA`-_J300000NkvXXu0mjf+>pV; literal 0 HcmV?d00001 diff --git a/static/flags/bf.png b/static/flags/bf.png new file mode 100755 index 0000000000000000000000000000000000000000..ab5ce8fe1237a18d6809a5570024eb108cb14a3e GIT binary patch literal 497 zcmV@|4`Xj5kLT%`al?B=W5I`(ov;U*021-Lr1*!W5(gW7O z@cTDV13&<=05Ky_HBiN$KMcQt819e>gHP`g_|Np;&&;0(C^$RHS>olZ*Q41X9v27}ar6#W9}0h{^{OajIK0?h(y z_zOe~zZd`lhy{p$|NA4!EeST}?;nUxkP49VfMx;1;V%&U|N9rH;SU2q05LM8GXV8K n(H~UyA0);fa6B_H00bBSQ>$p&WfB*UmB!B(-`wPhZ^#?@C^U5EbbPyl_g=3Hc01Sh${{P9=HB%4| z34*<3-m=C?^;L%miR{xQv2*hU;8+Y&0Dxf-W~KjsusWFAC4@j0#9_j;X5z6SjRhH> zd}sd(7FPhVFdSi!*Zj@;_Sc`kfByUdk|3A-`STmZ_yb~qxIp9o{E-p)bLt=iKmf7) z+|Ix(`{&<1pm88mK&}8g1WG~}e||Ik`OB#Cw~m1UAb?na(Zlrn4}+v6P!1@{@CT$5 z&-#|8y{IY2iKmf5omHz(^qAp)ym^JhN+jk7Vet{H$(Z8=>|AV=V zva&!M00M{!p&AHub^kXuF^G#pRQ&(-8^nMkpk@XJfB<6o_wOIv5C1P;X6Wwu|KUB< zc%X*g2-OTg#J~U$KumBafq;(A|A`a+OH08N{X($;=pP0KfB<5Fs|GR7oM!0i{{QYB zSm|$=!=N@Wf}I2qKuka<{f7YtBjf)QCj1u{`~UM7nrdKh0c`*VF+c!8{Q&j%sZ;-Z zd;dRshENS5{{S`og~Sg)05QIL^-54s5b7kL(8GuS8yfz9{>%Ui`+q-vfW*P*^_43C z0mK4S`s2qBh&}(NP5W0_$$%QQ@R)`GfB*vkck5Jby^kNv00000NkvXXu0mjfy0pt~ literal 0 HcmV?d00001 diff --git a/static/flags/bi.png b/static/flags/bi.png new file mode 100755 index 0000000000000000000000000000000000000000..5cc2e30cfc47452d5bef949628e955a522d59e50 GIT binary patch literal 675 zcmV;U0$lxxP)$wbBBJ zMTk#;k4<3e#ePFE;{?TciOx=-NpIdg`t|#dcz^$YFqm+of7AU<7UGr+0t^5F#PsJM z(2)${Jd)SmT+KY5@!|h>po)L5-!r~>3uJux|0`&Jz~QGy6a*CiGyVtq1|Wc#zWw|0 z_y6C2|NnAvaR2)A>-NX%|3Ci{R?!hvH~jzU``ypCK7Rkq&&~h)FDNvCApj5nF#yj0 z0_X_*{{Q{_|NQXv@Av-qBMmR$teo!O*6-%u-J+8r3n~2s`s?uP{Qmp>{`~+900M{! zNC9;}eE(3IS9-C{62AZc<>Jx>%af}0@?r&05Jg0 z{{#T;0Os`NWh`ko4>SM%|Kw(60099Kad7|t0qcf^`tR-^sIP`Fhru7lkI#w#>Hq?W zkzqQ+@08y_|9v}t;{CEE%)%nTc=-MC@P)7S@!~g$((Ud|+28Q224FCZIGXF0FQ1zcbfB*dX_lJRr z;s3wCNa|2D02KoSAbE^!0Dxf-mZ69ApkCb5fP^9ydGO$cm6w`kGj(t#`M{tFlLo%j z*4%mm2&CaJSn02SzkrtfWBLcO;r%bLy5Gnoqrh)qC;VKmZT`F#yj00W%wG zw4zx17##T6*Z=hN0Tvey7!|wG?K;V)0{r{_`}_X<{Qmp<|NQ*_`}@YSvI2-@&cAgG z*R?p0?_>V(@&EVlEP?{x|1esA`||VNU68t8zyJRJ_3zg&hTp&0WMzR?0R#}s1|E|% zRV}tRkH6o2#3;(nsVMXI-pgBmh17ms`TzTu!pfBl%*_9R;OEbWDJei#0R#}sGpDd$ z96Spi@~bn7G5z{~^_!ZLy2#s;5B~o8&G`G*|6f0Ap0WS=_rFd2C(ubi4FCZEF#yj0 z1a!1=2e0}MwgDi3SU@2S4mePl{012eQ4J-3 z|Mm`933BS6zyJRJ1;ztF0D(08|NVzmQWB*4&)@%$um=Y`lmy1W-@hDx{{qQh3=9AP x#JH7#;TJ3-|G=X54_FW_j)5c_0|P*S0RT0dOSD~(4;cUe002ovPDHLkV1hfJDkcB` literal 0 HcmV?d00001 diff --git a/static/flags/bn.png b/static/flags/bn.png new file mode 100755 index 0000000000000000000000000000000000000000..8fb09849e9b5712e9cdd8a2c25035da201535cf5 GIT binary patch literal 639 zcmV-_0)YLAP)8n_8%|dG+hlCr-g% z|9<~wU<9iE{r}%DMphZ1Jpcj3%*DZBWo-@72DFWlk(-gj@!ngb-$#D&C_i9e{QvLY zw|{?r|NHlgiHY~?^IHG`05Jg0{{(k=dF}7->+9=cXlNG}79t%T{rC3_t(pA#`tSX# z4jKOh|NsB|;rr`({{a5__y7Wk5omBxQSrlv5B>nb??0VZ8qCZrpS+?}gatn>o%-+B zPl=+||6st##PspQ2Y>)#{Cu5(QJ~_^?faD-pMV;E|7MwLubjQ@B-gLso$mVYgECpU zxpfp2fEt*XnBKpC4-fz`0M7pb@zVeZ8g>5r-~k2r`Sbhd;{WmN02I(80QmHEv)Ik{ z>H7Nm`}_MdF)?FhWi>T50*LYBMFw`s0-y$le?J(QfB$Fr_2c8e-`_!j`0PG|ckZ*_ zzkY$-`sc5Tips@{7Xbo@1sGC5IYwrQfB%2{|MTZR!ygX*-%McG4cZmTY ZzyK0(F$K@T-Dv;-002ovPDHLkV1la=J3;^e literal 0 HcmV?d00001 diff --git a/static/flags/bo.png b/static/flags/bo.png new file mode 100755 index 0000000000000000000000000000000000000000..ce7ba522aa7e948d581478432643c230eed1a658 GIT binary patch literal 500 zcmV^3LvnC{|x`%yaFl$ss8ha0V@6XKS=c-5cwO(_{}IL0ki=i zfLOjo{AE-9^-mn=h(G`Tfz|!{{r?|W+uz?{^yeR#!Nm9NlRN`J0I_^YV&Ikj@%Q)F z|Ns9m{0FN3^Z!3k!*8HnFvf4N5xas50L!z?-wXsAPk7$ ze<1l+iQ((fPYeJ7#KQ200VpKFA^{3Nph^G!{9^b86oDW}(Ek1R7ZiL9e}Ret0*H}e qBRB&8AR!3%7c6cef(%d+Aiw~vBYd|xMihYn0000j-HAXl7XJGjM{~r+i{r~sxzrQwG-&h#_Y&p;L z=ieV7_s^f-zyJOD{rC5(+EoAn1k&)I;s3Kw&;LT?{{2}P{Pq8T&j0^^J4?LvUd;UK z&+k8ffB*XXONL(tXahh1fi?X94^$0Q>Z$uRRO)Y4)uapmB!2(-ul(c1=C+V!kAF`) z$PCo;`_FHns{jIs3Fu;wy-Z(c2YwAz{&V4aXk?bejFO*Op&u`@i~PecqBpL@DIoojb8mb+WHGK%fG2>)a3=iVf_~v+`s?*1;!`?Kmai^I3i*ZIYtpN f{g(k500bBS@Ns{bTs^2gCr95H^s6Fo1}O2_S$Nfl}4;<}o-r{^R8Q_xtzX z|Nnu?|Ni^;2gm@D3=F@4EC#P1& zzuV{ie)#0qf2Lnh)j(~3VeG$ufPMl9Aczeh3x56n#l!R`HRQLfpFjVwfkN;%I39k( zVgne^e?jDThF>oL0*Hk{T6(^^8pGqq44*zhqZ1OZzkZ>_F(mFeH~<0+0A4S6=>Lb* QN&o-=07*qoM6N<$f&=*Yr2qf` literal 0 HcmV?d00001 diff --git a/static/flags/bt.png b/static/flags/bt.png new file mode 100755 index 0000000000000000000000000000000000000000..1d512dfff42db1ea3e7c59fa7dd69319e789ee12 GIT binary patch literal 631 zcmV--0*L*IP)?&DC3JV7l?ccjcgB=Qq4l4w|Q;eOZ4z|IjsEI`#PYSloM|AFHF zfy)2>XZQ=$@t;lU|F1tRzpnmg{PUOLH{-8A|F{4CvUU?d0I>k|0TuuM^_1cNUy$nm zzZn?*{^!*ACzSEy$NyhHzB0Wn`TXRU=!%=n%Ci4h82|!^32gR%glZtk^6USnb00o@ z{`LDW>%U)&BK_|-y-75RkKVi&O=|xC<=6keK+72b0*D2q;Xlai|Ns9mF#h@fM(h;aV>^!x1-zNu02XV-n@ zm3imZ@&EI0hChG)|NZlafdL?Zn1I0yH26P<=DUx29QQsc%}izF;QV-S-hYOl|1V4z zX|a-<=Kf#g*Z)7i{{y|szyJ_HEKEEM|C#>$KmJzoZa)M2-~Wm-e7Cl+vi)L|kY`}} z&A$F20RV*|BkB*O Rz6SsR002ovPDHLkV1m;fPLlut literal 0 HcmV?d00001 diff --git a/static/flags/bv.png b/static/flags/bv.png new file mode 100755 index 0000000000000000000000000000000000000000..160b6b5b79db15e623fa55e5774e5d160b933180 GIT binary patch literal 512 zcmV+b0{{JqP)O=a{vExMP2%`MCSoB^FIcLe_%lf;|~%E5I`(IQNh}3Ao>6Q|DFUXMn*>AqQd`w z|1kXd^B;tM|Njjl{{hM0zwd6?1Q0+hV1xeud-4=Wy?p-%sO`^#2S61Jzk!N?s)6X& zzhA%p|N6}=D+{y%Ab`Lc{sL9~1=0UN4*CD*7s%9KAf+JHKs~=eB-8KTKvw|-5R1&; zzd&a|ob(5%^Z$Q=wHy9p13+aOpFRNu5F>N&`Tk_-7w>=n{RejQzkfh&Kn{rf10?_b z{tFTZibx5v&dxav5I~H7|Ney-|DWN1$%1FyagzUW0464;_wU~W1Q5$TW@eGxtUvee z3vAf*8|igK9~@*rr66bh|NrkNM8z+V zAV?>O@ek;bKfu6d00k6 zPW&$KB`I@TtA?2x@Q~7pdcWi#1>DDZ2>MuG0I@IumHuaV^&7|soAMvT0IK){RtF@1 zgH;3B;_Qq-34j1%{P^)BFE1~|NkA(gBv!!4$aw$$eSiRB1ga=2D|__l(H|iA4T2Ex z>lc`SQ9x2&UjE?0g8%`<0`lvhzd$}*14J4{IhY2@0~G@V5J82%q9;EBlXu0U&@_82I`BfBC|Yn~P8llm?myQ3_K18=`^X_ivyEfB<6p_xd%%#*GZ! z-C&_V5IGPDK|g;X*gypg3;+Sd!XPTj0Q7fK5>&$Y{sHB| zjK9DB00sa3QR4q|tc?L6fSCSm+3>HW`NxbYpP7GsX8sK%KS9yYPfWi)GJ)AFzdp14 z{>t>1fsFwmfLQ*Ci!yxq#8guv=_mQ;-(Rp{AoSPbXFRCwBA z{KboB21 z`ppP*BG3|`!vO*asNvt=e_;3gWsp&PA@E<2jYs6ilRy6-zmSku5?~OOP=5aV@4sI_ zSN#QP00zQ!JD>SpAKYgR_QStF|9~F;2M|Ck zz%U1e+FyoWe;B_0VVclq;}vp_kNy3hKMWrj7*{kqJbMcaQ;_Pve;I)x1Q0+h46j}> zeiN4E1lkA+>|e|*mzJsW^M3)l^W_^hf&c&c`Mv->`48yHFF-%s0|+3-*GLfwiRNz% m3}3(u4lrUsie?}H2rvLBbQv(L;??y40000e`b1WGV} ze#gbd^&bp=ef-GykAYc$@BhDltSqdeSy?RXJpcawy+5fJAb?mHcsPJ&{d@EF@4HtJ z)qno||1xd*zaM|Ot4f#|8A1AQ-~Pubz$7IBv;-i4SQvovfB*ge{fFVtAE4aNE7yPi z`^Eg=5yP(^Ur(Q5`TLjM%#`8#_rHuB|9}1hItd_vSpNO`#qjs<|KGp=|NIHm@MXqK z79K7}ZvOwQ9Df%s{`~lX*xI%KzW-!k{Lc96Cr|@G0I@Lq`0?-epMSr9|NHeD?&umBG}6iH(DSogJte zDERNk_kV0Zng09%Itd_vSb&=T{rt@+B*66dJ0my{fpNkvBm!jpV_;xnVgd@XDk?Dx zh=IJyzyJ_HjJJUrz)|>#fr0nRl^;KTz#|yK`2G9$BS%LzureS32rvK^u`=B;c)+&+ O0000Vbu`-)NXk-Y`p?2$B#e1 z{(buQ`}e;;zyJNd_UF(2w_nb@VUiUP2igD-Kuin_BL6vl`zgIQV*mR8|8G|2U!T7I zn`-)Ow$bxne}9LGfB5z5*XcJ*K(;aeSD>o^0st`p&i@1e@&c~g4N@7~{}B9V!5sbp z`j|K8<@Nq-&kt=P>@pAZ`T7Q#;RiPl^j;kSgdzX}05Jg0{{#R8=f>y-z~LG0`u_d< z{QLg=2?6_k)DH^)`=sFo8YTUi-v;{r`o8J{6Wj>^)c^vB2^cJY|Nnmc_5b@He}DY@ z_51JdufKoSslWR1>-W9)3`r;XfsXn0`_Fc($H!lT!V@5Xn1Igu{qOIuf50&L{qg7D z?|*(LofMGa_%_k-*{}b_0g?9mMSwvE)Bq4bEIr*1})*zF_+U)&LMdEMOb{Nb-vOh6FrNC(tkd{&4;N%g_D$&)>g> z0^hO}m?z%iIsOJ1mka;_#K>R{i_RZNQ42x8VJSe8f#Ern2@qfa%&RHCyvg>Q00000 LNkvXXu0mjf=TkSf literal 0 HcmV?d00001 diff --git a/static/flags/cd.png b/static/flags/cd.png new file mode 100755 index 0000000000000000000000000000000000000000..5e489424884d2ec9e429f70d69af00edf242a077 GIT binary patch literal 528 zcmV+r0`L8aP)JkPqeYl28iLgD=0{><0$P44T5yOrT$dE?(KkwMFdoG^-J zGv9P)Kk|i5`lcNgUUAbboca5{hI)v&h!9!~`Yg)Ld}$VwYqqXn@gVLi>3LSVGm1W? z3qnDJAk6chH(u7f~FohUBCxfQDx8?5BQsCcprAnfVhO SHC~zk0000@|NZ~|iEi8i zh~?j3h8^dV0y&$fY|i?}`2XK;hJSzm|G$6#|L@;V)&KqZ3sU;$?>`_ZF38C6g#jRd z7@s_ODJ3Nev=9gw8UKSQhX4P)z5oCE{eQ>y|1j|H-+x9%#`EXT0R#{W1H*5i5@u$g zs{agN2m-%;fixfipeaCQpFe*F2q2cU!)f`}`5%6M06O*WzrTN({{8*;CkCkD_wPBk z=llWket~@P=ieVecEOTuB>(}$vTge|RYlbYUmpDW_v_E!KfnL|{{8n?G|>28zowm@ z_UrGj-+zAtrGTUoztWctUjPD#h2amw@BhDn5OHz(52cFPg zot=T34qlT?57Mzkg5?0ssOG0Ftp>paW8OyZ`_I07*qoM6N<$f+2k} AjsO4v literal 0 HcmV?d00001 diff --git a/static/flags/cg.png b/static/flags/cg.png new file mode 100755 index 0000000000000000000000000000000000000000..a859792ef32a02b41503b5ab5f216191af397e02 GIT binary patch literal 521 zcmV+k0`~ohP)i@P`2b{=v9^Ktp0uZ001fLMSM)r!?%mH+B91 z=ii?+(ckX8zrd=&8g~5t-|*}I50LQ;KzRlRfB=Hn08;wv->)Q*Kc0LLgTdsU-~VfV z{r~#=|1XdRMzE6r0tlo5X#B6gzhXrG`18Y*0!g6iieLY~Kvgqz}`W zLWO>Z3Lv@r7+CdZpiy8Ae}EeP0z(_<2Y>)#Vfe-1#`njMUlORAfdMG=o8iiD#%aG9 z#Q!h|feZ$rzkeYJ=p=vuV)VVru?7U|?FE@Zm|i>fBlzF zc~M_qKf%h=bAcoS;}NE7f8|yFe%B9?;;8%o@BeG_!|(4qhyo=(h-XBmKHXpc{~y!A z`THH3fsDVeudko)ARzm9UL&JI!+~uEM*rBES1=kd6zV%LH0J*N$gIQAc0y}k9qTFv z4h1oVG?rB#zNY^8{QUp5wE>>R#S4NZQd1i@F)*?OF@6y}@zmk^!Gr7L9asuAf!ae1 z{{CbBBH^Az=(+fAb?mHzA!MjRs*H~|NF=I z{~td;@BjZG-hUVsqZ#w(|L@=b|NQ>{d(*K^00G3pzycQk`jp|{UxvSb;p*Vv_V?Sr z{{Q;@@3$6WKrBG#F*5vR z`2XiW&+Xo6{KsbP_-SF+DJF`pL$*;0gaz z7NKVhyo`U;4*0+SK#>rfFFfafF5DUYXKh;r+@n*scPQUy6`!63S zGXn!71H*qtMn-04W+o=4|487=moEST#KiEAA;m(3;Xgy1iO{3BKc9a12~-UM7^Z`) z1qdLZI~bN-c=z}J|D88K{QmR*&tHcBxB*ZDKmf5Ya0q<-!`is-{h$8~f7m(x{QZSX zH3Jv`1P}|uy*o^wIGI2G`1kie)4zYe_&EQ6|NftW0ohon1}3I|Z{7d|5aVBMp0ssVo0Ek~;W>Z#0SJsA+2j`G% zv|UVeYYs-#Sn6_J90h1VosR?LBU7{U1rQ6+R0f9sPrg3=`~UA>#=n0a|7Q67_y6zT zP{j57|G!`V{{zu)Mn+knB>(|98e;E17s>tTLaMG-~Y>h{r`!g0jL-tfS4HmF#P%V_xIo5zyJLP zk|2W%e*G8w^1X|KC618W{eAoCFX+EI literal 0 HcmV?d00001 diff --git a/static/flags/cn.png b/static/flags/cn.png new file mode 100755 index 0000000000000000000000000000000000000000..89144146219e6fbec7eaa89e1bf4b073d299569e GIT binary patch literal 472 zcmV;}0Vn>6P)$bmtfBa_T{rmsVufH!rO2O)W0!b+P{TrwO zAb?mv&i(NBbu;G`sX*)cv$d*%>MiL-(QdhpkjakVmyu%k;sfcNRj;yhJaxT5MTg0u5&QfH8#Bf O0000XbC_7v4G9~kE;3? z$h?1GrT>4y7{3|*{r>Zd0U&@_82$jY{AFPH^Plk#hy=QU5o|Ds0oDLyFn~2M0QE2c z1P~*G;PKK9g@-SH{rUU<_aC4k|G@~v`1Kn|{`&=C|M{yZ!G37cNq_)iVfexD=MNA8 zh5p0Hf4_c1v;p<}|Ak}_F!%rh2&CcXZ-(!`82&@8$XA^gF6?xq}zd#y5N`WeVBbyB+frxZH0- literal 0 HcmV?d00001 diff --git a/static/flags/cs.png b/static/flags/cs.png new file mode 100755 index 0000000000000000000000000000000000000000..8254790ca72f98d9e79d94bdfcb8839b1fd434ad GIT binary patch literal 439 zcmV;o0Z9IdP)|s3jQ%L{DG0k=ogG+0SLgMnA-pV!aytqt1(q3VU5g2cTadE%W~lnu`}m; z-504zOD10s2K8+~RqF^K7O;8$o;>*sR0>r0`!|sM`x~eNg#P~i_4n8BzrTI~MgGdl z@-i@d0|+1%Q&SyHO(9WHu7Cgk{{@--A8b6x2B1zb1BygNIks=N00WJ{7y^0)%zz1kWJN^&bany+5X-;o*O`wU`+x5q12BMq=+`e0f-vAB ze||Ik`OBc90(3J#0I>iAmFf2%21!Yv97q%>_xm>^i2a8FLV}HlAfRG^0AlbRVNjfdgKH->)<41fMG{Q1Z53yA)}NHF>VM86o=*Z=~EMTLRklg;&|8vY-+ zh5rBh|L@=bKM?ZoKOp1JpMQV<{rmmr|L=eQfFkpA_5cJB<7Wm2?&_*f&z|!oCH%Iq zVPN&@1^xa7gnxhi`TP6#zh6KNfByZH;9_972M|CY z4S#m*VEXjw_teQhfB)LctFzlGTKpda#8eRa1F`|=V+j!s1_l{`0AhU0z`&K2_5c3; z?|=UACdB;@3Hy%?Ffai9!1(6P8-M@;JL%7#4}bn}Wu*W1^8Nqk|9@zxfJk8Yg2Rge z5=!5G`~e6c79IwMkAMHSMfCl8%kXF6O^EHl82Alg`~}6s-@pI={`>#;Kf}J}IRF8~ zc$$I1RCFdZQi0L={}0epF!T#d{sWT?V3G+SzyRi{Q|!NXWpDrh002ovPDHLkV1m+z B7Bv6> literal 0 HcmV?d00001 diff --git a/static/flags/cv.png b/static/flags/cv.png new file mode 100755 index 0000000000000000000000000000000000000000..a63f7eaf63c028615b2ded5878b5e14a7dbe962f GIT binary patch literal 529 zcmV+s0`C2ZP)*82p^=00=`tlmIeOf&y&dk@6oT z&CV8YCOfMzZ7b-;WHc4ffO0*K`UGc#9pHKW{n=1`s=Y4`sB0UGuD|F2(+4xFrs{6A+r|Fh*S$P>SRGs(%l z+O-QHfEa)O{tdAK=ovi517(5A-n@AO5I`)f0sl(Vm><0RZzq2;(QtLuuFT6X6;y@p zb-8u)9gSFj-3}5^z@SqUX4qfA01!aT3<}j|#!Osn3@^V(zxysO&hx#~ZN`f)vS*$d zpLlA?$HBnM&cM&k$j1o^^oKjY0t65vgZ)>ehy*ei5K#*ZyWju7F%Ll?01#jRixNU5 T4U6zw00000NkvXXu0mjf>cZT~ literal 0 HcmV?d00001 diff --git a/static/flags/cx.png b/static/flags/cx.png new file mode 100755 index 0000000000000000000000000000000000000000..48e31adbf4cc0074f40e95f87c1f103b91fe270e GIT binary patch literal 608 zcmV-m0-ybfP)Uz~~Q*L}q{yKmai^Fs!d`Rbj9@@=xl~KZbvQ|NZ^*|L@=bfBpc` zKOp-38^rz{DEepVv)`L`Yyt=%76t}3hHqR?<~aN{I`!9n&u`}MKs~?z|Ni|Cihlk3 z1w?Q7fI4M?HUI<=3&<7!82Y{{Q;@?^mDN?|=V) z{r~^ll=rtu5CcE}F#**BRkyY(adR>Mc_H{~T5^XRV~oVFSgBv#s=t2z{grn0SJIVV z|Ni~D^5u8)5(a<(0%_pmV<{@S{pr)+Nt0E6|KyV7{&xHKulY}Z0S*2Ic2cIyuhSoY z&3*a@=p=vu0%~ALNs$10lZEA9e*W!WzkWV{%9A7hYlh}ewm<)V{rd@2{rBIm&Idr} z{RV0P2mmnv&i?}d08{q%2kh+oUtbt0DGA2L@2{yn^Xv%Q%_4hjZ2Q{KfBuh*0fWjOcoPYi>{Qk`ejtlm`TPy!Q@c0|K1n4Ay00P>u ul!4(JQe+}W>@P40kp+Sq42=5$0t^6?P(4CrvcmZQ0000s1`2Y9I{~ve$KWO@Yukru?KTxgz{s9CK3*7Y} zZC_6Qf4TJkuQ&gHzW)Di+kXz$|9k=eg(Cm|XMz~|=g%L20Ahjo4`KiVr|y5|U;qEV z`p@?NKb!P_9;^R?@o<+hfSm*oKp+h;f&YIQ*bg)L>I;A4WVP82*P-nR{;Wu@%{Vvyu7@~4txIf`R>OD+-01E zIfRil07L2S-Mat*#Q65@TRuKMWMi;}EJy%|ff@h;2;_%%@7_UT@edf{0+7H22rvMY Ws9gjvbyTka0000? z0048MLcfb{@Lpld*gfdL?Z zSQvhRtN^J#x6%GQNHxSfxHgc;AD{-1tAIKH0tl!9q}uD*5!1Kl8Kk6va!f$;fJ%WL z`2Cv^NdEc54D$xi27mx!VfgXqT8ME7!~2)OPy-`SXu#NiAkhzFFflLy1Q-A_8F>@M S6G{sJ0000h<6BFn%a z@b8~2SoNP@zd$;E{sbbRuHQd?{QCI=sNwhbA3*&Qe}GP900=;09NYi^fU@pUdVa9*13;+Sd!tjgXKhXQEMobL97(p6<{RLvMGBN!7 j!N9=G@a-1^K!5=NcXWu!7_DDe00000NkvXXu0mjfeQx^H literal 0 HcmV?d00001 diff --git a/static/flags/dj.png b/static/flags/dj.png new file mode 100755 index 0000000000000000000000000000000000000000..582af364f8a9cb680628beae33cc9a2dbe0559f4 GIT binary patch literal 572 zcmV-C0>k}@P);we;9uK`S<6~zaM{qBL9B<0yBR7V`E_e2q4D) z|Nnpa!Ep64!{fLA8NdLj;oraifB*mg`;Xx-kn#6Fhzn-q&in!pKrBENJRA&WD*ySo z7*5@0`26EP69WTC_22)0z>5C-g{l_hVFa245I`UeKudt6h7^Mc@BgDW81BCO4-x~L z`sXhc{R3+I%fRsKA3y*x{R6rHsA1>M|6jifb2E4w{z{xB{`~z5CPC;Y7bk|9<`c2PXgjR$^CT@Hzz$KrBEfF#?VC z^aSdB^z+g5SJMxCJOGqNsQwQkfr0#&=?~CJ009Kjz|71H^!MIRd#Ajb^76;aUyQ$y z%m(TN#spBq-#`C>zGeUjAdrR+|30kwu=eoBL!3-pGMq9%bs!`E|ACMovw

4;Zk2 z8GbPU1Q5%#7t@Nb6*GKbU;u{yA29j{CVzn$|6qa)V3LCYAiw~8(_SNKujRx50000< KMNUMnLSTY(1rd4x literal 0 HcmV?d00001 diff --git a/static/flags/dk.png b/static/flags/dk.png new file mode 100755 index 0000000000000000000000000000000000000000..e2993d3c59ae78855f777c158a6aae6c1fb5c843 GIT binary patch literal 495 zcmVh!ZNvLM`<}kPiIA3?K?Zl!VJuS0ABN12uI2v;s z000mK68GQM4oDR3?|C6;zBc4LR82Q1eETXSa+3nD0Ad8%4|Ml`Fn}2U{~ypshW{9V zk%{T!hYtV&#KHiVV*o?2zW>+&Bgm+K00G4EikX==E9>w5`yf~S`o*<00G4K6dZ++hy)_Bw{QPEdi2K7 l5H1Kw2asrHVqgFWFaQRwS@oh;XP^K8002ovPDHLkV1foV*8Tth literal 0 HcmV?d00001 diff --git a/static/flags/dm.png b/static/flags/dm.png new file mode 100755 index 0000000000000000000000000000000000000000..5fbffcba3cb0f20016c9717614127b89db4c9664 GIT binary patch literal 620 zcmV-y0+aoTP)yt}{r~U(pZ|aU z{rk@#!dhJXj+vP`D&$&!2yPR?Jud5I`&tqo00z_V3?cpb$_)%#~li z5Bw?p&G@T5&5h~tB<;UIJ-`3_mgbQL+5iwhOdtm^{QZlh+Tg>NcGo{qR#UR9ewx4g zlzHy^uRp(j{rmOj?;oHBfB*n70M7pb{ow!s5r+W$=Kufw0RQ~_o$a1N^GPoTT-yiw z&XGVBSbe$r0&xrf|M~#~9P-zx0st`p&i?@b004Y@cH`sX`~3X;`}>j`1PlZ1WGkKd zF1~FN+w&9T}cJeJULy4V8r?0tNHM6WN&<2126ppbC05A-~q$vMCOd&N)3^rn&3WaiZo>@dB zxpL5=L>h@#UjVT%{Q39iA58Ths0J3s|NohoLF&Lt!8(8c18V>XAjZFc|1vT%{s#lF z^Kb%2CZ>-cJ^%y|<6Q;@;r#qR4;}z*|Nr|B$h_ab1b6QI%fu2>dIV_O?>~RRu82tM4=f|&Ke}8;oX5+eBw}y?G6)5=s|Nnnr@aNATAPEpaEN{Mj=m^OD z&%p5S|G&Tg{{H#<7bL;LEGj9<&cFmz{_j5mJbLs9Ab?m{m{|TZ{D1lB9Z2clKfnI{ z`ThHs^2c9)q;CF`l>Eoa3N#g>nv07INCE^93jWH3lKe}28-VENu!{qxMpm&svFOiawo%#4hT5U&FS z5Yzu(KY%Xz`RDh~-yl6N|NT~b@qz9A2lm1$Hf43G`D-AmnZXVNY5)izCWas1fbRMA z8?52ykLBVV-&q+tkKFpbbOC=r`2SzOfQo=l0_p*hfB*gk2q2IKpzD7Eo%H+Hk6%A8 za57|Q3rv;f;d1r)FDv_xg9F*eKs^8f#KQ37)2~0jMR){${rwHH2k3S7pO3`Z{#jf7 z{|`16Y&=9YkOT-IMh0&|hF9+yelRe6V}O#tcxPkSx96}BCoe=1&?OKCkOT-Y05IoG U$(*n^qyPW_07*qoM6N<$f?|9Y@c;k- literal 0 HcmV?d00001 diff --git a/static/flags/ec.png b/static/flags/ec.png new file mode 100755 index 0000000000000000000000000000000000000000..0caa0b1e785295d003869330fc4e073dce07e7f6 GIT binary patch literal 500 zcmV1sCzZm}g`N!}J$oTi?|G$4gL7*^D3`7C}Kmf5Y{CmeN)&f@km*M|^ zrvE_l-~a!AA&BAspa1{=fXIJ9!9O2vbOQts3j+fX{b%^|8m0my0Yd-4N`WN9@BjaR z=no^SIM8~400P%s(0a|A0FHhM?*o#se9Q zEZ;b|7ytr@MWm#zEz$bb`!9d~{{Q>$@1MW_!07MqKOpw+zkh)g(B$8L|49h*Ov!x= z5I`*NZ%IAX?Fbu$UUAQCA3`wxix_2=I& zAouq_SzðCcuS!~%52Kai0?gF&VORsRAR2~rJG2PFT1^!)w@)C_d-AAkU2Vc75Z z*R<@y`9z1vevIh)-p7{p`5C+7f|6l;f1_&?! X)GmJPc-xs)00000NkvXXu0mjfGFPrC literal 0 HcmV?d00001 diff --git a/static/flags/eg.png b/static/flags/eg.png new file mode 100755 index 0000000000000000000000000000000000000000..8a3f7a10b5757b006948ea4436fb242d02dc9a4e GIT binary patch literal 465 zcmV;?0WSWDP)LAHVtk{r`=k{y)(2e*gi*sIRYISXlV_^=qKp{(!;n-+xj9 zUjemETFMXP0$m6sfwJP_;%#kh009JYeOg-Dy?gh5gTXH_fG|KLm<2Qhs6|CZ<>JMQ z009IFR-loRl9E6vpeV=!FaTTi8)D~Q7yv2;2q2OXK!5=N{?|@pNV(X=00000NkvXX Hu0mjfG@sA` literal 0 HcmV?d00001 diff --git a/static/flags/eh.png b/static/flags/eh.png new file mode 100755 index 0000000000000000000000000000000000000000..90a1195b47a6f12c70d06cb0bd0e4ea88d7bfb03 GIT binary patch literal 508 zcmV`hKmn*~pz%QT=MPXRKmaj;?1ifa0xhloNlE|L zuK&--1mwx?-uoV+`qwW8u#*4+i1F_GyFwg7fByafIr{%Uh)$p>Ae%w(|Nq~=m~|`# z{`_H7QhIal96$iEC^sml1*F~kc<(nb4FCQ91q_kDz!3TkBLDsd`Sky<|4qNomi`s~ z`xoeVfB<3v8uI)9A4wibp!A=AfB%5B0nwj-e?TNKsQ>@@`|oeijK3Q@{{o!^5I~F! y*$fQd7#Mzm(H|HCnf(WhfND7yc3x%x2rvM-AWsdQI)rrq0000&(jx%j7OGE_~DVuFcQkgj@33fJv($pjj zgoNxWFM>pG#K4X+%S_Ys!f>f$mib36%ekHNec$;y5njB{!+Y`YzVGwA&4mTVh%ikU z03gD2Hn&LPeNu%hDG7PUvzrphs|^(-as$IIo1LmPya%Hc0Qn*6qc4XX3oKoa+Z)_XBQk8 znPA)XelBh#6J<)fj|w>7X+~Yun^@Bp4$+N z6L8rb{%QnJN{fql*fJH1L*2YjUlB~CXS&&LY)1V3h&68|x1_5-(4l3HUgs~3JvLXI z$_D=zL{dTnq9RK`-w~w|sCYqqA;@OoAE0!{9Gi+cF%zA>5*8OAiXWs z!A~!@Tb_6WJ;mn(q~>CYJ~Oq(|Mc`miY)G1d$)?S_lf*=dz3nd-8+hwz5w#U=!7L- z+Ve0W8Werm#o=KvYxRVVNtM9!poHk%m;Y{gxKdXC|Y{ fc0^aUlspXz7vm>S7OoCUZUIwXLGJ6E%Z+~lY(hhH literal 0 HcmV?d00001 diff --git a/static/flags/es.png b/static/flags/es.png new file mode 100755 index 0000000000000000000000000000000000000000..c2de2d7111e3cb59cf6511dd2ab045e824bdb43e GIT binary patch literal 469 zcmV;`0V@89P)@|4`Xj5kLT%`al?B=W5I`&pe;NKW0^Ri&h`xRJ_x;0v zUa?=y?0^3M|NZ~}FE9c#{{3cP{Qd6}13&;Vf!z&M{pZWqKYu4MFm$tgedF}w=P#IQ z7-9gT-$11R0mKA$(qEu4%ok$*y!^wMRm*x;`R7|k6yu?K{s8?55I{^|9{?Tjhec2I zv+6&FhFWG_BbNVc|Ns94tNRJp!0`V!Py;{!F#$2enB#XZaohg-5%Tlk#oa&nzQW9g zl0Y{D4gK?n0U&@Fe=;yIr=|V7caH%YEYL84k`Tt9-wc2LGODP&y?7BIfLMT@X8Qey zK~fSFpuiXa$^kk7RCwBA zWQbH``0|MX0{;DB`1Ob3-!Fz=zZw28fY1*HhF@R=VQ?@21P}|ur+3w` z_wT=dfByab`{x&s{PXYspTB>919^Y{{Qd`I{N9v10U&@_7=ExZ{APUe{`KE~Al1MB z{rb!Jhml3<_uqeCzux)%bv7|NkFAvw@aK@N+YWG5`bsF#yj00sZ{|0ReUZ0OJ4u`~d&_lgxzd z_7grGtKje;)ax=32IqJ>VE_O6|Nr{|0Uz@6!2*a0?AgCSJ_s@V{`!jztXlEUzkg2h zOW%AK0ILQg2A~)NKmdU>0L=y=29PKt(~s@sMWK)O87MU|4G0ImPt*+y7s`{{I1L_{;G9FVHyv z0R+;(^!pEkq$JpwzYKqVGyVRp{reTr#sB4{{{Q&{G@Ah!GGGK$3=lw!Ux87Egcwk{ eXi`7`5MTfy3O%OUuKb?>0000SU^I57#IKohy|#uQJDd#=|98& zUrgMLRb~JG{$u$2m+|*M=0AU#{`@s%|H#1h=ih%I`g`z86F>lpk-UOTf}FCMD80)oDIYC+&q4vMR0s0&DpH|L=c>-#`S^3sJ`m zRPz_88i@Y=|MmYr*an6_AO0`^1P}|uzkh%JGXi;k8UFtPIt#4m|G)nbm0000FP2AE)Ir2{}>qlLBSs|`Qg(SfB<4)VqlOE;Q=cD|Nnn$ zna02W|Nj1E&`=Tpav_4q;M$#E00G4E4{SI@`q`VGKvVzz{r4XmU}R+c_2(Zz0I~c7 zs`v*r?Dty(;z&PFFXX zRA5t=4x{1SIibD)Vqy6A2V^D4P_SySA|L?j2ip1XFA)9%V~_%1r~w2J3=}{2Oiu1 f7(RXZ0uW#T>&I!FfdIJb00000NkvXXu0mjfj-u42 literal 0 HcmV?d00001 diff --git a/static/flags/fj.png b/static/flags/fj.png new file mode 100755 index 0000000000000000000000000000000000000000..cee998892eb316c3293ef2d52afec9218bdbbc03 GIT binary patch literal 610 zcmV-o0-gPdP)2C05Lr?a%1G+Sb3M_ z-f!*)-&mJ@lxC7weD@!u;s2li|9<}wjr{Zf&o8mqKR`Cn4*&rGF#yj01QaSLwCD}R z0w(ww8v*|PzTN}jB`Pj8{QK|!{{8;|gOCLd|L9jy6{oELG6Dcq@B)aDq496GGsCmb z5T7wXTnzN$?|=Wl{r{i6vr6{G)xV#={AXc)t!L+QBoiQjSb+Zc`=1dU2n>I~p8E|B z6OfY_{`1$ji1Pn`9_T5yZrhJfj0}g~00a;V(9A!7nZZWFG{8az7^c++|9dI@cmDl* z!Nvb)UorrL86bdIfbsI1fk}{;;V;BV|AE?oY(}v2K-{x07*6Kx`SfB<4-V2A|A=r2_C101z~ iU~vp#0R6xN5MThlzdwv9U#bcK0000}CO1*!he@c;k+ zfB*mg{rg{#hxPwIh8G{d0|o#7{Raep|AEAFCm#U_AQp((@4x;AD*Xo({rB(3@4q1y z(m>63@(hzyJPYWMEi%>@iRTXyxzU zAo>qT2S^W413&<={Q1WKlmMyz`(Hzv@8AD_R~~+N^7b200Z@m&0zc4{Q@7rNwftxJ z^$Q??fEu9g1Db8CAq;fMkDq@pJa`8*&sI~^TtyHla^%`8uswf)HUI<=3()%@Pl0^! zf8Ui?K|l%XeRX{Qd(n@juAHz(D*15I`UefB!>$cK-hRUqIf!|Kj}Y zKt+#Ue>r^ZHOMRf{y`i940V72V*2%m0pg)Q5O4ka4>U8*PAAEH2(%ZZ;ol#C0AiF(UW*)& q3=s4Oj6m#vP&UxAe?ZIt5MTgFMVEBke8_SD0000BE0lK=nzFYgc)d0A2*B+AFf z2joHok-@WP&j1351!6Wt`q9fjf1W;g`1ALFY=DuG5oiNI0I|ST{|2JJ|Ni~?`|A$_ zRt*pr0t5gt0M7pe4IJopi4@}M{rvp?{Qds``}+I+|3-D|`uqR;{Qmm<|NHy?`uqO- z{Qn;q1i_Qs0*LV@1A}N|@t-FT{{IC^{`vn0sPGp^)o&2vABgb_!eEtCyu9%!Kmf6* zGhfda5_|CT&#%8A#S0%rhKer*8VNG{57cZ3sU*g7is3Rq0I|G(Bf-nd3vr@r@vHy8 ze*OIQ@9-oMOb-A(eJ@7=Ab?mP;SCW2x*O<#U%#>Y7zqCS`2!F@APw*!ml9!S{vjhP z$_zA&0R;fLP(1(v#Q5^%OL#2G%0Af7VC%@R_vTF*lgG%);U`26kn-@hOg zU%!6+4+cOs(0HIde9xZz`}Onxub&LUB0x(30+2WcIRJn#2ut|?gWYu1Cf+!-K%B8# zdf?1WA}#uZ8oj7u>$I1i0Al&`=O0k%-@icgAIJnM0xA6maSq6BK-ECw|NZ*S`0Lj% z1_pot6puj;05Ax`F!=umqj7^frO?t|3^&I1kxUq9yECc+jQpY84SWH_0#pxl$?v~F z@*hy-KN0|X07U)z`4{NpU%#2aHUI<=%a31wK(7Du52Oc(|3O^?R1IN+RRjI-n*kVB z3=9AP#PZ|EACPLGJ%9cJNh|>9B%spYzZw7h1%?tp0I_@ndg9MNE>313@6R75NcceF zkr51-#U+7;F#`Sf7i0rK0I_`g_NQ&Zk)EZ(2O=d>QH$KN3zEi7S9u{+2K>GX4ds`2QcM z=+A$K-~a!^(JwH9Fn%*K{{Cdb01!YdV9)*qi~a#?`wdg{8%Z^Y!NB>w;@|&31~6!UgVU(k2*|8J(R-+sudaynhucHbwAMTnor{mwqO^w7JHzaBsT z{O^B8RYf5+LvDs&KmRKVd78=o{`1#HTiEo_OolaGleS)G+IQ#sUI`b*pv<`1zCJ=H0jd{{2S>p`ri%{LsXJ%FbMS z$#S`6f|?OG!^Jxczkf6Q`UNF{l0Sd`ad7zm>({^EzyAS6{{CgrkluOb3l1A>ZU2~A zK+FZ=zkmP!`TOVhpFbzBzFaPmD2$N3;+$pK?>zdet`f0002ovPDHLkV1gy;I?Vt8 literal 0 HcmV?d00001 diff --git a/static/flags/gd.png b/static/flags/gd.png new file mode 100755 index 0000000000000000000000000000000000000000..9ab57f5489bb9ebb6450cb27f4efe0cfb466144e GIT binary patch literal 637 zcmV-@0)qXCP)@|2i2MUNJEAGBA`g{QJZ3ub1IpEW7h{~Z4K zJ3#Qa=Q1XS-;5h0el2+U>(Te$zyAID&HDTIKad810Ad2W{jW5`@9y)AKr^5J{4Kqa z;cw*UKQW)B)-pW&@%z{RUqB7N{{H&&_Ycr?fB*vd;rG8kGw%JlqX`uK!_D~nn&&T_ z*-Q+-7;nUX=6mt$`7e;3-;BTi{QC{m01!YRC;j>JdmqEEKMa4Icz%Tm{+4F_^}h({ z_1{sye%WyRp84|E^4Gur0Kxx1e;6150*D2Oe>41%=Kmef^V^IA7&yOx!2%AYU;o*D z%dq`!;`!w)_PhDb-(PS30!;@9Adn3rpZ_$9NHVegX88Y?;V;N+#{WPzFy?-P;*ar< zJ?CFrnZE^h{{CWM00h;Fvzl@K2fHp9I6dqaaxb00=Mu XLcuQ~?TP?t00000NkvXXu0mjf`7udf literal 0 HcmV?d00001 diff --git a/static/flags/ge.png b/static/flags/ge.png new file mode 100755 index 0000000000000000000000000000000000000000..728d97078df1d07241ae605dff2f2cac463be72e GIT binary patch literal 594 zcmV-Y0^8x|9^h-OG^F+g7@$L?BC01YQ`Wbb?43;K=s|G$6#bNnb!1Cx>Qe>QfY2m>?Izi&U8 z1o&Rm*8l_%%a6%3nS?}u4*37)&;Q?l7=Q-<`}?1Z>;KQLY|KmGQ@ZWEch0Jnt zUmm{%2p|@g=ujpTGX@n^21dqzKYxO4`1a@2NuYivJ4XgKw*UYBFf%g!{qd7YP>5~& zE`R_4F#yj00OjT7{QUg;`}^~|xBB|}`T6Q!vcs262Lz;t$n|1+qbnVARhhy8{z5C(*C%JTg?tEV3%;s64O@&5h$(1-*>2%Ak`A87BFlP7^( gh&l)WvH=1N0MfQja}g1cO8@`>07*qoM6N<$g4hNuZ2$lO literal 0 HcmV?d00001 diff --git a/static/flags/gf.png b/static/flags/gf.png new file mode 100755 index 0000000000000000000000000000000000000000..8332c4ec23c853944c29b02d7b32a88033f48a71 GIT binary patch literal 545 zcmV++0^a?JP)lgG%);U`26kn-@hOg zU%!6+4+cOs(0HIde9xZz`}Onxub&LUB0x(30+2WcIRJn#2ut|?gWYu1Cf+!-K%B8# zdf?1WA}#uZ8oj7u>$I1i0Al&`=O0k%-@icgAIJnM0xA6maSq6BK-ECw|NZ*S`0Lj% z1_pot6puj;05Ax`F!=umqj7^frO?t|3^&I1kxUq9yECc+jQpY84SWH_0#pxl$?v~F z@*hy-KN0|X07U)z`4{NpU%#2aHUI<=%a31wK(7Du52Oc(|3O^?R1IN+RRjI-n*kVB z3=9AP#PZ|EACPLGJ%9cJNh|>9B%spYzZw7h1%?tp0I_@ndg9MNE>313@6R75NcceF zkr51-#U+7;F#`Sf7i0rK0I_`g_NQ&ZRCwBA z{Lg>@|4`Xj5kLT%`al?B=W5I`(ov;U*021*0XgD3^5{teN< z@cTDV13&-@;@|`T5QYI@3O)ok?1DO<2trehc#kXh!0Z4iC6of!=I9L4Jz5Qk(jP`l zJOKo8(qFLXAF#IH8`u5XwDI@PAHNy@|4L4RsD^0x1N0+605O4m05bkR14QCiMDQ;; z>0h$aKjWi;+@CNFzZm}i25JBZAQt8_hOB_!_dovn^Y72^zrTL{{r&6TuiuWpfB*e$ zwD}j1{Ph<^0%eu?|D0`P00`k|9}4iHT+`$2p~p=WCoxfpgkZGj{YEt g{DC2GLI4Ob02tU}a;hkw5&!@I07*qoM6N<$g4!w08~^|S literal 0 HcmV?d00001 diff --git a/static/flags/gi.png b/static/flags/gi.png new file mode 100755 index 0000000000000000000000000000000000000000..e76797f62fedcbfca8c83c51951680d6a6e9081f GIT binary patch literal 463 zcmV;=0WkiFP)VoB37QQ+R{;bN6GSUi z+kb{HA3p-oUOth}-@kwP{0U71P%%INK{Y@H82)oDp3V0Dt?T`39Pi$;RTl%zL?{P4 z2_S%&kW~Z0x6qjPzkeV9`s>}VU!Q7P|1&Wm)PrpR2q2b!5Hle5FfeebsWZ)5spT9vc*+3O2}Iw&|Nr{sqx*C2&8&?7c>c4n{QvVC zD9-TjFQbI?+i42`0*K|`>)%Y*uQL4o{r~rGhChE9{`~$Pz`@Ka$@uLpb; literal 0 HcmV?d00001 diff --git a/static/flags/gl.png b/static/flags/gl.png new file mode 100755 index 0000000000000000000000000000000000000000..ef12a73bf9628ff5a67b81bd980d9c5d2b2c0f05 GIT binary patch literal 470 zcmV;{0V)28P)J{teTOL@z0+>00G1VRSiT77W_YWkm2*^|KGm- zfAHXcOY8ruSJ7+$Itd_vn4oTd_U!+mLkz$F{Qvdq|L@-*^6S_C%a&nk00990)`(*=-xesBS%qG0|gf?f~#iu^9N|j9|i`10Ac}ZUz>% literal 0 HcmV?d00001 diff --git a/static/flags/gm.png b/static/flags/gm.png new file mode 100755 index 0000000000000000000000000000000000000000..0720b667aff506d7892c5c301af04e6bbf932751 GIT binary patch literal 493 zcmVwRhhvIeu00_fCKU~B)yH$s9sXS^B!W{?M(W&}hPbMwO z;*cg65E@7haJ!!XVgYOW|Le(9kkY?@fpY);{sqc`6amR!K*q2CzkUI^Y_hUI(*XjA zMdSH%VNp?r|Ns620Z1T|}fB<6Q zlw#oF`Oo_sVk+2%KTsoq3?TP6gz@)3Ki_`_=6?VI#CZSdeQ9y&f57m8xf%uh13`xW zAjrhTbmsgSfB<4-$Y)3kNW1sx-tWJ^f#}!YUqA$5fJva>FJQR-`S({vK;>fVMSuWe z0mcW=Ig;FxKxv@ppTFP`1*!N0BL9M&0|dYz`1hCL7Xv^5F*2kxF#KQuvOqEU3km&! jiTr^fV1zR<00bBS-TrJ5MX@2w00000NkvXXu0mjfGz`_@ literal 0 HcmV?d00001 diff --git a/static/flags/gn.png b/static/flags/gn.png new file mode 100755 index 0000000000000000000000000000000000000000..ea660b01faefde01ad2527a6abcf7d1a5c1b0526 GIT binary patch literal 480 zcmV<60U!Q}P)@|A6>41A`El{SSoRd}EL3@|NdrR`1}9=ZwUJP z@AvHwzkdDu1yn7|BMY<#Ab?oFX8(t({tZ$6>;L~hU=2XVuU~(E|N0Bk07O6y00G1T zbT=bV^`Afg|NLS2{ReI~M8m&-NE-fuGynt;*hzmtW+Q3%1=j#1fvO=I{`~y|)Bq4b zU?=?r84r{KY4``%041R|`~%zYhXEjfz)k`h|LYgXRlk0r+3@c_)IERx{rUUv4^RU@ z0D&|xgN*;p0Mzyy>QQ8EKn=iP{qyfH5CNS85I`UeOuzpyNJ@hA{P_#yFfjaPWc&?| zr{By>f0X$D{QV0G@4r9|{}=!Qi18~pg5ikaD#Jf9Xfy-Svu_Nh0nj)GNi#731Q-A_ W8E1tdJ(&;y0000P)fLk0D%}*I7ff3uKv?i+N*~ULWZ>4 zW5%k%a3T{@*`z6pma6eF$JtK+F@C*&o=d^t|Ns9GOCXH@*Z?CVuP#7nB5Y|1kmu|NQ^=?>FP0zrYX%2q2aZH;)P`n*#-K1r9WbfYOYN z??RUX1P~*`M}`*mir*mb{sxCVG>rbhqT(MY2L1y54rHu+wi6(L7#SX-$0bVa{(;3h egu%oB5MTiLH(5{VMZMqv0000u-`~Ig{`~p> z=MRwl_xtx>F!}G#@4vq{&D;bKKrBFA+{}CzK0Nsg1pog2{{I_D14*DX1pWH^3y6RW zSzcL&Zwvqd#Pa7K10w^wlkmIISh(4kI3GTJhN2$mzJLD! z0*D0|DC|s(0(}1j8UFtJ4HA_S5@P=M@9)2VV#30}-~k05FvNkXnV5ck`2-L^EDTKl zn1259n3DG7^QXUm{{H**3#f!E|`n6Mz6>fhdBj29g(UfB*aM-=Dw#|NaG$fByXc1LXhxFC@THKjjKQ05P&# zA9gbr+SsEBRPB^?1!&T?30hEBFhHFGv5AR^>DH}B00G4E=NHV45I6k$@0N4rAH*g9 z{zDN+_&*OP%Y{RC0Ro8e#fvv0A_7PTA~XKMG0?q08}8kE2oOLl>koag&}IJi^WT4% zN&g{c!yE%t3}J9_Fdy0V1t5S}4xV|TB*XjR%dfvcU;YDm6wdeu;Q~GU4JP)J`S$qz^!f=A3G?{>83*rF;63vB|NHv-`~3j>`uzF%_#GV;x3_Tu z05Jg0{{&$2KZ#9J4f5RdzRdzC6Ae5p=(o+T{uB7y^ZNSwN=h$cVmJ#62-nx+ob%l!TKAE1V~I16T$|E1+i#l`u&y!03t z7ytqQF#yj01Tv_Ick58z;rzG}_y7F-%)*RRAv1X@1^f8;`}+eA4+yBm#PKA>w6y_L zQ&a!|00ICp0M7peY0>I$KneW(?7rp&{QCm?`vUdU)NRiG`uYI-`}gbY@;Dai8vP{| z4h7uY=>Px#0*Hx$0pxvffc*LW3+PFp{}_J#0ty11^n2!vLjod>mX_Jy|Cz2{eHy3% zAb?mH7=HZ$2N_TWSP@V&gaITO7A`uvc=2f<_uIFxDk_pd4FCZI)bI_+mz3lOl7E0f z{^u`PGlT&Y`3GeD{rm6lUtS;y)Bq4bz=+dkVE6#Ehk@Y-82x6z3jhKP0OI&0DF;s+ Q-T(jq07*qoM6N<$f)`^cRsaA1 literal 0 HcmV?d00001 diff --git a/static/flags/gt.png b/static/flags/gt.png new file mode 100755 index 0000000000000000000000000000000000000000..c43a70d36424b66f1627216ad988cd23a4be9285 GIT binary patch literal 493 zcmV|9}7f|M~m>&%ghRD!%{(5DNnfNcHoNKp}=7 ze;EG#|IZ9j4hBF)cVB@t{Qmo2T96TF4?qC109^ty;2+Qi2B0xObN{1)Uw^{8{`KJ4gZ0H zP(J{D2s9NSfWR7{27t^!x8NVhN&g{E`Ueb#e*ggla?+pwj3Cv27=VUhwE^V&zaVoN z82$hR5DUYve}Dck14V%vK+Z)23?LgAK*7ZT@-zbjKmf4-BkT7+CNVw+pd~+kF)%WM zL>VCz0|PT7gS-eZm>Gd?16jcE4PH%g~!@=<9&!2x_@aEGW jS9>vVD)6R*AQp(xFW-Wt{{mJ2|NHk};Jc-)O#kPzME?5A`1{ZQ-#|To{!0im{$XGM z2p}eq*?*y`{{RjC%V7Pj<(}hvo^Z>=hrJ-xK=d0#0&M^~2_S%&fR6tAp8=>2$p8C? z0q7F5H=q9h`}OesIT^OczkdG%sRkpU6i@>|0I@Lq1-k@j0La}yZU2}V|JX_T{{Q!% z>EEtDfBydd!vuBJUq+w?fB<6o2X+#W53~WS;s2M<|9d72Yseq)`11cZJHs!aS-+wF z1}X*!ASNLG{ST%Ze}QTk7ytqY@{}>p)on&BOV)*xm;om>72$%sP!HhqD7ytr@1teTu4J7{m`^Wh2AGglm{~UiA z82u;s5_9PZ|FH1)~3de={)r1*?FgUtsi`kx>?C4?qC1 zfX)8@|LZe*d2K>lZN0{{I4*^b@QIr~;(tA5`a`e}Ddhy$ldQAPvkwr9kh3`~WiS7Zcc2 zh-#qDKOj^7{QC<;3||-k0tlpm>GvN7NlB2NKYyjTe{-?^h8PLd@aHekus?tQg2Ee! zfN=^CK#X6(5e!Gd)(i|h;JEn(j5jcXFhHq*fkB7?Aiw}&uW^ngBcx#f0000J&k9ol;AaCAG*Vvs6lsG2f+AJUecp&K4&zS7@MzJZZ+RCHJO2~-cn~)8*ZB# z%#~(Seaqctb3On>xdArM!+zLfe2=iS%3k1HK82I)yo62#|&;D2*%o~N(LQ$HrxFU=@<#wgQDty7s|5?>qxBTrc>UoBZ!}1le z#)a`Pq~$aEPO=D0fO80I7h5SSMqU=q48*j9Qb*%7#+Pi|ervSf?0bSFwKsAPn1FO| zKH_&kh#AJmvOUSnl~!1AmcaNJM5awz`0DF46>zWZuCh$z(7uBp0to4w2iu-uj zV9oc#M;CkJ!OT_8;~(;r&Cw`0K3r=(%@VWyiIA#;S}+n)^}q>|)QZ|IaYyyY!;frq z6mATysX~aM!z!n$rJ$=27fpoIr3iB{q|Gr32uDRa3PcNj==OQGHve|07^1DbtUgzuEQ=j%rDF literal 0 HcmV?d00001 diff --git a/static/flags/hk.png b/static/flags/hk.png new file mode 100755 index 0000000000000000000000000000000000000000..d5c380ca9d84d30674f05b95c2f645b500626c07 GIT binary patch literal 527 zcmV+q0`UEbP)00;JD`K-EmLvOuK( z0R&e6?>|)a-@i;iz|8zVIqAQ;I)|_@BNM~FU%wy-s0ZjAfB<3vx(uZH&mV?Ae;64V zIcsYEzkmP#{)7J;N}0Ju0muUq%~ z^Jl0Zz)k`PASO^y0(FCg{s2v4J zf4+RlSW)rg;lp2_Kl2?q^5yYkpazCNzyJOD%k=jzP%%INf#Tuc?>~%^l1w0DfWH6z z1E^V4lvz;l%d1x`a&jQQ{ROE8h7C|LKmaj5WMKG(8n4KVKd5of#=rm&U;y%qJ?5>3 RVzdAN002ovPDHLkV1mTk^F06n literal 0 HcmV?d00001 diff --git a/static/flags/hm.png b/static/flags/hm.png new file mode 100755 index 0000000000000000000000000000000000000000..a01389a745d51e16b01a9dc0a707572564a17625 GIT binary patch literal 673 zcmV;S0$%-zP)>fJ3En$GhGS>sbE%%m3$AD)q?8M9y>88-}kR7#RKlk!P~Y_PLuF7~U~3`~nC7 zF#yj00ZUDdpLsm{7ajP|&HwoK0Usg|6%f4L_{`Mi{rvv-`ukf=Ed&Gs-sA7L!Q7*a zj{*QO0M7pb%?Sw^g@yy{>ihEY{`vU@3=8@G0rvO$i3mOL`~mv-`W+b$Mmr&io5dg< z5v!7q0*L95jt`TzK8Kd(Utv)OSp_aLv){6ccV+Z`{Q2+asKUU&aO3`Kpz6wW8wp`< z28M3{0mSqnB#A*-c*8%1=RD#sSOwMznKA3=e&iEzwo{cA=PgXK`2OQ}gqId83!|%* zA_Kz@fB*n70M7pdECCwp4H&@R`1|(w-}M5x*74i)0}%fAt;XafA{48))#>Z>?CD#}*e}Ret0tl$#*RMZ7}Jl7Z|M45`5*URzH9L z{rmSnPy;{!u>dsyO%meg+00000NkvXX Hu0mjfN{&}S literal 0 HcmV?d00001 diff --git a/static/flags/hn.png b/static/flags/hn.png new file mode 100755 index 0000000000000000000000000000000000000000..96f838859fd2aed975f5f4134050fdbc0486ce1e GIT binary patch literal 537 zcmV+!0_OdRP)yNpn^YtfB*U? zE6K^g@B<)#SlTBTcsfda`|@_0R#{e$UZ3l|Ic6l=B#}T zWCl5lg}RFY8S^$g{qgfJOdc2ve*glAv3c@IFK6|y-~NDH^$#cn3{a5k!L9^_5>O@B z$^W^zSlTD;0tg^R28Q0WdbfK|zW)9V43odV{`~*->+kR1AO=tbO#T4}-G3E1?u#4x z0Ro5x7#++k42m+GppXWk{}2W^;6Y*k7i(1vOT1`b$6{=&w9#5#oJ b00=Mu*}Zhb7k&Za00000NkvXXu0mjfKokPk literal 0 HcmV?d00001 diff --git a/static/flags/hr.png b/static/flags/hr.png new file mode 100755 index 0000000000000000000000000000000000000000..696b515460ddb670acb7e9de4438aaf21fc5fb77 GIT binary patch literal 524 zcmV+n0`vWeP)@|4`Xj5kLU3fF!G{fyDoR{}_Q36Vv~H|Ns5{^Z)NZrr*Dqe*gae=g)r_`DNuw zfB*t(VEF(4$y0{Ee}M=nS{mB(NB;kxJOBSE{F_{n`2Y8>|G$0##TjK~fi?gH5EIz! z|8Uj6|NiY-l-<0|OxA@1MWlzkmOI;leLR z$De1;{4g{7^y$;LZ{NOt{rct07a;lk`E!5(f@%OM1_PkhC8`eE9egqWbga z&p;hdpFRZ$Adm)#>fe8W4*Ct02B|hR1*-n0rS<;(dx&bFRY1n$$BzL5h>;=uaC^Mc z(+@v?|NZ~x@4w%F|9yUZW7+yTpo@LH>e(bUfFghX{rmIxzpNnpimU?w0mQ=a>kp9s z8>AGffmuv7DrFgv!3yU7{`2q8f1ngFoPlxn4AoS-S!;e2821p2q{((sbfB+=MK@k8U5Cg$|VB~~? z2XKWZk_lAZtGhi{|56nPieMKY$Bq=4KgZ0muK;2JYWn}5;nka8K-GUCa!{rJenZIL z|9<}gF~mh#ftCOS5DU<%|Ns8~1)2?0{RgZLWF&|Ls)lL+iU2hL1Q5&LKMX(>AUTM^ zNU9+S#0FXN@8@rz^Zx+^5DWL07wmsTIe-5EX@IBzTJ`52%kO`z5F362$-h7b*KaNc zh6exv#P}EJiR%3Sk01R1^NZmZ(C**=fB*Xb3rzn04HN{CU^bJS()(Sf00M~R4FdxY z(0f3MKYtkh0!g5OAQFsz{e$TF`x|H}%fCO*7#IKo2o$W~FaxWA8VofRr202h8w1#j zz=!|{Ah3qte;CCj89_$={rBfLBSbS$5J>(7`GW}-*g)q41Q6q6a2)=FMdm+9l%onl dL?8elzyJ+{hsuy4pm6{I002ovPDHLkV1hP90PyYjz{{0J*12TXlP$`i71!VmC|LYf!%PK1iv=Z0I@Lq zVgLrRB#$I8Q2qeT`3KSlX8!?(3s3+U9e@9T1Mx3N13&;VGFUSJ^?=Y13Wr{r~&-|6d^ahvDC! z|G)qK`}^nrA0Ybu|2K$nd)6X=0AgWa1{?O`IRi-PU$8V7{r&w9sOb0ae<0Pr|Nr{M zEF}%J0U&@_82&N|Y=e#y>zg27mx!0Xgp5*S}vr z{r~fq0csvl>92pk!P<5VDfB!NI2#TFQ3lKnzfB%Al=06ZHfFW+c#KiRe{d<4_V)^&)A0s0pNIe5S)eu>r zF8~6F38(?TQZ#J<0R*xEXct5e0}KG|WIzExE=U%r7$AT^8h-rv@ecwRzz_$3Xaxu` Y0RLik?wUgPu>b%707*qoM6N<$f;0ZTz5oCK literal 0 HcmV?d00001 diff --git a/static/flags/ie.png b/static/flags/ie.png new file mode 100755 index 0000000000000000000000000000000000000000..26baa31e182ddd14106e67de1ac092a7da8e4899 GIT binary patch literal 481 zcmV<70UrK|P)1Ab?mHSU}=WzCQi??=KL1`SXRBmG?g! zeExSVgb4YXfjasA0Ybs`#&c5^XvcLUqDM3{{9AP00d^^H2e48-+%sM)d02u=%hct8G!N( z3;+QH((o5-_OE}xfO;@2_y=+i*h!3FCjkTyNW*WSt$#tPfB*dj3@CIxKqoQ$2DuvO z1O^6x00KJ+r1UogVe!Ksu!etsL5P6?Ab?navG)7lA4zUWkT?GPWdcP410y3N0|YR! zFo-FE!v&-P=p=vuVq_>~VE6=zV^DnmVAx)=U5ZNz6vaS)0m(NHWW2-wfs+9Q00bBS XO2cxg3=*#z00000NkvXXu0mjf|9Z^l literal 0 HcmV?d00001 diff --git a/static/flags/il.png b/static/flags/il.png new file mode 100755 index 0000000000000000000000000000000000000000..2ca772d0b79b255872cde2fb29060bbbbad950f2 GIT binary patch literal 431 zcmV;g0Z{&lP)WlqUuh`uiUU82D1+EBLb>EWz|Nj3k zj6%@>aVJ0ku|Ql5RsEk~{?`9D9{=ZO{V&1vKX2lHHJSgJ0SFC1p8y096I?Y|?05b{XcgblKTHZ zfByjpAQrd=h&?HOAa>`R|6Hv9XB30N3RxDY7$AV4en1PH(j<7uAT&Tc4G=&q@-F{c z8i9e$01Rv(35=ybe;NM%WdxES!M~uG0dj%y@b5pvikg1_0mOLw_HE>d#AF}?ph|!M Z0{|%qc@l5wel7q2002ovPDHLkV1m6PxaI%= literal 0 HcmV?d00001 diff --git a/static/flags/in.png b/static/flags/in.png new file mode 100755 index 0000000000000000000000000000000000000000..e4d7e81a98d705da8d7054e77e7d311805659678 GIT binary patch literal 503 zcmVl^KlW*80IEmzVa(K3*_6 zG7fg0I9Zj&0woGah`r_&Kwu3FK=xChQigwjfh>?7kc!_h@)sEWW@MKI+5iwhEdRtz z89B8WSj7JS|MwrL=l|b3uZ7Osk^B4auaUxSRgtG4v;Y11_x}$gi|9Y8?EnG9`1|i) zCPv2p|ADsrhuF4k`@Nr^zUpfTpS$xp!A}Wj4A3Yb2~_s}<0pUsVqyY2p8>1`g1&zJ zsVvR4Yya)fUw{4wtNss>0tLxGfB<5GmnRH!zxweH8<(Mq0N7e&^ba6_ z7#WHgIs!VLeti1p-=9B!fB*jb=l8$ge}LrQ-#`%%`S%Y9{re-sFSERHIY0ohF#KVF z2*K4Ml>Ykz*ZJq)UtlmW{9*tIAQm77@e!0?Mfa}mS!7%=x2KmY(S z0M7pbTSu9imq`K?75n7n_u%3MB_szB3#hl|A07@E6$<6-<>30S5egmg@cSkZL+Ix9 z0st`p&i@3O7{&wE8wU3C1oia^`T7CwI=ckcau@Q71iA1L?!*;{7^Nm*&$?GLJd|NjPb3=0E0KfCb4 z#nAu(05Jg0{{-+tQ26`)!^rI-{P_e371QGlF%9qW_x;Sb;QRdk{r*xJJ*zy?_rWgu z&$=$1lYjzR(%pFe+p|Ni$IOoG{eTR0XemVUia$~;LXXZeDO z0096o0M7peXKyl1N+90g@dyY5ARP|N&gc{s2^<#+#>?j*9u48*^A9U5-*(6(kqBPq zC^|QM0*Gbi#3NFYyicBf1{wyk;Wx-spzFb0kX0fvuWZ`CzyHEKd)}3G%ew&r05Jg0 z{{dZPvvzMc=Kqyj=IHYZ2nGGUe(f)J7ZwZK+v5TV s=!a5B6v9X#`iH^&14cjw13-WQ0BQ>oQ(TIK+W-In07*qoM6N<$f`&OQ@|4`Xj5kLT%`al?B=W5P+gNxB&nJfgpGfTOih;e`=>T z5jZ8;?_>v5xi(~iU^udv!6f5$jpNVh2O?$m1OPDr&i?@Y{r&#_{`vm=wBfh>{Qda+ z{Pz3$!R5rm=Ee2+`0e-d?)LGV)1LD5^!4@i=jZ1F2;wA$U5|F$_;B;f&rf1p(qbG! zCte(9VPa-y5lGpbZn4ZFhzcT9$vfdqmuG z@j$ZG>u9())mkwqmYHSd7eFi*FJ3%$?AX0~_kM%HFED^GKqQ#;=g)7T_f%9=fX)F3 zAdr)QMoCIaf{X{6{|BNG$o>N%f#5F;02KoS5XlH2zyJ$0KZ{``H1_}i002ovPDHLk FV1nFR>VE(L literal 0 HcmV?d00001 diff --git a/static/flags/ir.png b/static/flags/ir.png new file mode 100755 index 0000000000000000000000000000000000000000..c5fd136aee534ecb59914e336cad18d18ead2a4a GIT binary patch literal 512 zcmV+b0{{JqP)r;gUH{1e{Y(x2_S%2fSMQ?7@vH7`tSc=xS~J*|Ni>>`_JFszyAFKs{8d9NdA)L zm1AIH00;mv0M7pew_3Ln1`-ek5ajjb8VVZW^Whu|9pCfc910uY_2L}~8{YEX9t$4Z z@!Kj9D)d(L0*LYN-@lBEj6f&-|Nox>4F7-s`Ty{t|Ns8~x3>Pz!S){pfXY67`UDU_ zOc38f#US*GW&hv2{?Eqpf6;>f$N=n5fB<4bR}BO)G5?=F{eR-b|HMQT_5c3^H2?$< zb1geNgNn-kGiMln{`!CM;{TsNL8PAke-;*?JV+Z*eYOvhv$==KunT1sF?AKYlWZiGf7_{AKv_o8k9wMiBcC z1B3*kzkfmK*Ds)AfB<6r3XWMgVnF4hNdW;sfB^vU;z%SnI0)(h00004s{hykP}!$Xp8x`g@iqg4NJaUd z$B+Mm%>4cD_iu*Zzrl=O|9^qF|9<`Y547mdFIIVlOMCYL1Q5sui18rvfj0Ph3vJwt z)dnUeruXmP0|XEYv&@95W{1bGfWG{@sKWZ+FOVO6s`df7U=M&0& zKrFzJ{`2=AL>j0Rra@ocD)4hB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrIztFq(O~IEGZ*N=lh=;=qSyMwWku z27eMOW_|f0uQeek^e_9g7KH|eq{JVG0^UaP2Jy4}|L^JL_3uCblfVD@mnS9t`~UCn z|MT(+KT|KOfz%~1cG~~^_2vEk{SFNQ?Cjg~|NsBWy<%2im{vp^YdeS=hk-Cyo6U`Hgs{l4FR(w#{P`G0;a3G2@YSmzI~fu(ZE1> L{an^LB{Ts58L6#6 literal 0 HcmV?d00001 diff --git a/static/flags/jm.png b/static/flags/jm.png new file mode 100755 index 0000000000000000000000000000000000000000..7be119e03d203695325568174b72522124bb2f12 GIT binary patch literal 637 zcmV-@0)qXCP){QLU}0{l%6`#}@@_VD{9F|q;xF#yj01gpgWJ+A*m zGUYZW{T>4SpXmF{^Zon(`}_X;`}_MY3j1pm`Wy}V&B*(tydCWT00ICp0M7pd0000m zGd#J&@cQ@tG$8vu6a54J`qTCN{{H)06Z&u*`VIU0o2UAMn)~_v4c^|~0*D2u;qTwS zKYspMz5CDEtAAp>e+Mf5)@1$t_wR2Fu3uNK{_0!&`}CDxK-+|V{{|`s2p}dP{`2SW zZ!oxe=XdSY-}fK=Qse!l!T0O_!(X}WAk`4?=MOLh7ytr@32Xz9{pZ*3U(#Z~DiVIl zOa8j^;n%JAKjo!$j^Xni)Y$z6WB&k?3JlvV7}WR}00ImE Xyv9Bjb9W)}00000NkvXXu0mjf@Xt#6 literal 0 HcmV?d00001 diff --git a/static/flags/jo.png b/static/flags/jo.png new file mode 100755 index 0000000000000000000000000000000000000000..11bd4972b6d5f134045d4e8ce134601ea9b5654f GIT binary patch literal 473 zcmV;~0Ve*5P)M00|Ni~>`!`VeZ#eh`f&K#;fdS|pfB*tJ>FU-06DIsWc#z@uumAs9{`&>~&q~MQB#;&cfB*tH z31sk|Jq+K!KjiuK-^&B5YLK~LCjkTy3s3{|pFco7yH$Tr@L>D>cm1y|D}MvS>F@7f ze}Db{_vg<)5c|)+zsmedM_Y~p1Q1BWd$vDo!X?isvq}Pk|KA^w>VH5L!1(y{_x~TD z9$-NK{r~sxzrPHB7ytr@v6F$JJdlAwh=Ji34E;f3{DCq4fk_4ifB*vkxQ1J~H9>i| P00000NkvXXu0mjf0T$ba literal 0 HcmV?d00001 diff --git a/static/flags/jp.png b/static/flags/jp.png new file mode 100755 index 0000000000000000000000000000000000000000..325fbad3ffd3075a4a84d8d898ad26ef7d3e0d56 GIT binary patch literal 420 zcmV;V0bBlwP)9whYk?f=!Q|Ns8||JN@lTD;`{R1ZWk|EGa3dAO8ObDh3E3Cb;oH_5X#1 z|NHy@|M?558b}5Q|Cf`4hZv9q2p|@?lb|{i68>{>{ol0lx`{mi O0000=G`P)0NEt6k^VGA)9E1hT9ocRoN>wSfaWv)?-raRm?)Slj<6Po6w} z{P+i;{`~nD6~!ec29@~tkBNo($fnHz0mS%{fq}QS{_m4#|Ns2?|K~SQUHy-* z`HU>AfB)8feAoK(-@hL}|Nr_0bQ_Dj+^xMk00K}f2RQ&hFc1JYLgN3=lsJ)M+p3uR zWC5ydAM*!ly4H1hsiEFvIU^1)HH_GYz!QMsIYt5i1c4ZMLC4DfztdU-q)WGxxg;|L zi3%uAJ2fm~N8&I2%AdL;`{4^9#;e!&D=C-&Lk8;9|NlO`c*HPy9@C${KeOWnB;`P2 zATRu9VP-jSWFD-S8kh{*zoh2aO#pT8A2W0o(w3 z|5bef!~)j#|KF3RAf-U``!@sYUq;#A42-}3UHtv;%kTfcfBpOQ3nmTE<|1vNq z0ns0zZx{dq2xP;5h!=kY&G_~A;V*`-zy9<8Vi5ZI|I4p`PkusG11$kMn1KNxfWUtE z{TpHc!>?a|&i!W8{>5bQ`~TnHf3N=fz5nHq%!z>% literal 0 HcmV?d00001 diff --git a/static/flags/kh.png b/static/flags/kh.png new file mode 100755 index 0000000000000000000000000000000000000000..30f6bb1b9b6c5bf355f67a17531fa73beafa6639 GIT binary patch literal 549 zcmV+=0^0qFP)P;@arD~1pHxO_zPr1&>t9y%wPZrAQpyS3=Ms1K-T|%K*j%o>i_=z2W0&D^Y8ax zhQELQ{rLl7|Ns5_-|C4+00M}Gf#D0s|6k8u{RAokD*W^JKSaeZAp18+HBcQ8{rdA) zTAYJ{;SE3lu^j&CtN66?*W<_k{(=kvTJiVSPc{h&pyuy)ZZrJ(`}gOM|G$0#rP$=; zY#H_d1P~L*>3>1SGXDMzbOE=49-9EaM0J&9T`emwH;=g~P!Ocy*DnU30tNVGcpOKXK;IXGH`G*aB%Pig@_cF{AXeP^XnG~{rU5QfdL?ZSim;?VE{Sy z7c(ax6CWR+s|%BWAkauYPfsQR0VXz9sPSMM00M{!7+@fO0fqklmFn#S3Nf;>{s#gU z7DjgV{|pRrOO`MKef9?wUO?vn1P~(w!@{x_lZQ{f0d@UhVEX<0FF08K{sNMJKnNIa zzrX(mdR{@6d*A9+009Ja65pra?7SkZU^!3-{)PrTC`>`Y0b%_6|LHH#J`sQb0@|>a n0T_AEh(trkF%3aX009O7j5IT?Rho+J00000NkvXXu0mjf2r}#E literal 0 HcmV?d00001 diff --git a/static/flags/ki.png b/static/flags/ki.png new file mode 100755 index 0000000000000000000000000000000000000000..2dcce4b33ffe1f40d490cb1a2e03efe22ea56155 GIT binary patch literal 656 zcmV;B0&o3^P)8t@U|NsC0_uv12fBrK4XN%za&+zB_ z{P)aXfBpaa=ii?{|9}7d|NHmt1seeZhy|?e|DWei8UFwK|L5=jKYtkh{P{kK2}m(L z`||tm?|;94|Ns5#-_IY+QnEm+00M{!r2OxHhJR4iK=k+TZ>A77)*#lue}Db^^$Tb= zko*TE|NI8J3LpSOb8Z9x2m%4nC$a{*Gqe90@enzQH`ta%tA$7d{Sv5iP&~x@8dMF~ z(;dfh&fyCHF#yj00oRR-9sTeJMNR(n^YxvS3U`G57YhIQc>VqR{rPSM@(uBDU=5F+ z{|5&5hj_*A{sI6o0M7pbe|x2IYDAmA_)R$v7XIy^!u#*~18PtP{r&xWnDPVw`hiji zey{rM^8J)$5m8%@0*JA9=57`qRaXnayHCFDJ@r1pR}vUfQ&&Fu_xE3Vu+-%{U$!58 zQy3!)avcufO?mzv0@a%gl z1HS+N7!IC&H?5`#AOHX{0M7pb00(zPR4BWp;5AJa{{8^XwcY>#`eSh{`1tyzm&^bF q{8L{tuE6I1;oe7AG|b!z0t^7P6ga05`yJ%~00001r;P)}L!W`0l>rF;|7So3KrTcC!ho;=0*Gbf0S0@;YBwbYEs=i=3_$ev|Np-X41fOr z{{tp}0~v7g%iT=?0mQ<{9!@# z-(SD~{`vL)_wRqdfBpaan?+XX#@15+0mRIp%kY{1qrJR?xs-YLhQhSVzk_f6;`sgh z57Tc3#$ODKzZsZ*F#y?2e^`Y0-&}tV5I|5HD)yDjz7nmS1pU~~aVF#A6wD&YtO00ImETIDprOD_2B P00000NkvXXu0mjfKOhx^ literal 0 HcmV?d00001 diff --git a/static/flags/kn.png b/static/flags/kn.png new file mode 100755 index 0000000000000000000000000000000000000000..febd5b486f3f90056637b23caa26d838fbadd7d0 GIT binary patch literal 604 zcmV-i0;BzjP)h(K@ANy8uaQvQ^_nWcs z*Z-GL)eL_?t_B7*Kmf7KbznHW;LqhtK<$TZ9ej=*TGLweOZn#S|EVB#AOzI#2dDw) z)4xDJ00a;t5ND<*{rU5ogY7p9)8EGU->gS|Gwk>cG!LX2Y%nmu8NlfEl`8-N#0ZOI q248)K1w0H4M?n#d6+r+%fB^s&Q!OA|2rzyC0000Cs@aq@DuRlQczuzDZ5@KTj2q4CTH~*`MftCON|DS>3-+w6c z9|(gO{~7-O`v>9vKX&5_Kmaj*WME|P@B8=S6~kYUnG7sU|G#_z>G}QV|KC3#^7rq5 ze}4b}^_xjb^7)+E00G1Tlwy4KiiwGVIVgltUY_yi&tI!o|Jl8p;n#15-@icU*KbCk z6Mz3^`1ON{fdL?ZnEnBs@%JyYzyH5mxBfr4|7*t%=AfYeY;6C2{Q{!DKY#uG1wwy- z+}}Xm3;+QH(!lWNFN3HElfM4XRjZk-tp4-xFo=l!{|(XrbkBdVP9XXJ&!0aG3;+Sd z1adCWtuJ2w-n@w=AmGpT?F`?)|9k%Y-_M^QML&N+RfBB+Dh3E3pdT0*fp#rf!j_o$ zUs8faP3>oG?f*A#7{O)(oeuE;(0C>wVqgFWAQqr4|9E)*J$m%5y#o|bz~KAxm4S=v zKP&4$usU!k0b}RSKS4oYgaHH)%c-;9wWL`3={r0|OreK!5=N4TMk7RCwBA z{P_JV0}}Z6?;n_iu%H+Q{s1vR05Jij`8c?M=-GouSI=K${m;n9!7aeW#m~(x$j!^i z%zXLG*~fS9it_V|2?zl-00a;V#NgMjUvJ&I^~3uQB4T37ii-d5-u?gQ&wmw_XD?o^ zUAsm=P*7M%NJdr`Xazt3fo%By|Nn;%AAbM-ZD?YwEG7BBxA%WR!T-FR|8><0Vxn$d zUT@yKdH&)BP#Mq$fB<3y`hl076R7Rgt5;mSyo`Gea7>-}|M%}Nf0%y${3VbO@hKwm zAWK?W z+TGp#<;$01Vv<1pdP-ancZ!Qkd3bmLHK?nrgX5I}Ab=R3zkQ1wk#LIP517FKVgLC9 eRt>}e0R{ktF&Q^6#MUGL0000@P)xg`upqGzh6Lx zERQVE2><~E((v!!|G%IA{@M07EI*V_ln=0RjjdN{oM|h)7EO z{{7?6U#34`ML_iD4-=RKMg|km5|FijfgS(|AV#2u+YAh13=HqUkqe1m1{eb(!T=Kl c0)PMm0G()MDW>>^I{*Lx07*qoM6N<$g4p`a`Tzg` literal 0 HcmV?d00001 diff --git a/static/flags/ky.png b/static/flags/ky.png new file mode 100755 index 0000000000000000000000000000000000000000..15c5f8e4775b2b68e0360c1f4ff1f37e61611276 GIT binary patch literal 643 zcmV-}0(||6P)0{QUt82Kj15^L?rd`uqE`oNxjFF#yj0 z1e^fI2i-9Q(8&Vs@%;Dq2on+Z{QuwM00sm0@b~}!1OzrH#hk+X+Tt}E6bb+U_yPbi z0M7pcvI;2tA|wYPA^Z*x0300&EGqR875n@A_WS?(`uvdQq*oFTSt1&p=b-!h{Qv;{ z0st`p&i?}PGCBeX39IA)-~tZg`v#`;6$Tz2`uqI%`ThF(|NnA-^wR?L!uSAshx_~g z0000205Jg0{{tif814-hz}E2g`1#%M`@GEN)${-9=jr|Z1Nim(?*qH%B39iMC&AR2 z>ggNf+PVUWW##cF5^jI~{P}h24a1+`jKBZ?zJB$|uU|aB82|la`WF`1`0wBUe?VZ- zRnf8uXahh1u`n?Z4sYX?@pO{;x_8g;0tl$# z2T+})81L`j|Nel$KOpz_zrTNegN!(Tli}h6pb7teeq#K~3=Aa(fB<4-V9 d!wLWb3;+YKCC*ol*cJc)002ovPDHLkV1loTFLeL_ literal 0 HcmV?d00001 diff --git a/static/flags/kz.png b/static/flags/kz.png new file mode 100755 index 0000000000000000000000000000000000000000..45a8c887424cff6eb0471f5a1535139b965e241e GIT binary patch literal 616 zcmV-u0+;=XP)g01!ZoKn?Fz`<^k#?O_Q1 z`=8<8e}=z6_5c2{{QvRuAM+0emcRf01JR%V|9}7gKjGLHfB<4)U|@L0AiL;yFw=jq zasNSbGXHKG{NMlfpWxpAK41Q^gEaj6FV4=$@arEy05LIu!2gi{Gync$`1$|;-~a#q z|NCw7f0xa__4fbPn*ZDO{onuJ|3JnA^#F|o2q31v4F4ql-&_7K9cVD)|KCjiesKSP ztM&iLe}uy71TZ z|DRv){|H?Bqoe!x=x?wkfBu3(0w91`fMN3Vzue;gQUCt^|MZVr;{RRU{|DCmOS=6} z@%2BU5C8a_|1a6~&;1YB8$b;J0mKBvKpP_e^#OhS_CN2(f86)~DtrE)Yx94t!v9OT z|MTDe69TJd_zQH>zdry01PrBrQvYt3|7!q-C@@&>{!_XBPnqZ6E6@K61pYml`M2uu ze+NdW22lI}1Q5_kK)3wPV4bt}e=;a?{xkdrMUmL?|7pP3c>kYY0b~&4U$AjN34j1% zWLW#P{SRLy(>r)v0s|Y${(}_LK*N79FfcIy1Q-CnX{(%t#68R664G@6GIJN-*24NrwB3rW+v!ydOp^Ef6{n_)(btIFVjHa=pdp6)} zz^!@$h^2tpKUmc4)64h&|Ni?2LVth#{QKwk-@kwU{{Q=j;qSk{fBpc`pWlD@C4}l3 zHUR_xF#yj01d}3g9TFqw{rUX<|NZ>{`TG464+L38761SLpR(uO=J){s0Q&s>`~3bJ z6bIb^kphT`;m^O{e;I!LWBmP>@yDNkj7*H>@iN-VTtHNrD96Ue^ySz8pMMxZ=pRTg zKmaiT)&Bj@@b3@E4ZnZ>72sjw}n?8s2>T{qV)lKMV{%|NQ?241(YPff@h;hz00Cp!a_N2HF7% zr}y9gR8D)C;wJ_Sx5=xYz5MWpi4mv-=yjk&KmiL7K#aZ&_9^w5@1DH=3l15e)xUl~ z-0=7B?|)4HL4sh3f5LL21siGr0*GY=!$H*K#ZR;BJ~dv8zS`wDeeIR3;>1y|KC3hK=RLTAp0Mf zWcc&@%EC-|_DIyI;S5 zZoa(f#*3@};Q;9GfBygi2&4h78pxV*WHQhn|Nk-k{`>pa-{0-~I{v`b{|EZ(4?qC1 zz%@X;G0|`0)6dV-R;K{9rJ5v|%9;KB_nVP{8R7~c2@pVx*BKb3t8)H6dH@UxP=NgY z{qNULVEFv`^^@@rIK+N~gX`}f7I~!;+fM-m5DPFa{(t+%C?E(7W+q^;{`t)a3di3} zzd^yz010JK%>4cT^&8LzfB<5=h#HaqkRlk)Wq^@D01#jR5K~0vg#SK#00000NkvXX Hu0mjf%Ubyh literal 0 HcmV?d00001 diff --git a/static/flags/lc.png b/static/flags/lc.png new file mode 100755 index 0000000000000000000000000000000000000000..a47d065541b0d998da832e1981b479097a9b36aa GIT binary patch literal 520 zcmV+j0{8uiP)#-NSZgwz{Qh@GSmETWyIzrrS!pkkqDDw`CEYY|A1Dxfxke?UP9EWUlaZ{K@! zXYTRdfWyFioqIm+xW_M=V(hYvbO?~5ZHkEt37xh?rY1@PqM%`P+i2w@_wXJJO#nzA zheNt~+w1B3H|fqGMx=x!lms6SMy;uqQ4R``NH&49%B_cditsDHI6DHXLNubec}E0~ zb8dy|txA`rTkZMN{rCM3FV`MqlfuwZrz$X1)+?ntF|UvTkD3M?FxSaem74ag}Vzy5sA+`s~GjvDl6>(E?=UGu{=w?r5#MJIwhn?GrT#s zeRSo}v&#TUmcjL&mxEF>2%EIxN+SI=O=izlM$T5JH;yv-C%^zTfK|9CLa`qJ0000< KMNUMnLSTZ|5$YcR literal 0 HcmV?d00001 diff --git a/static/flags/li.png b/static/flags/li.png new file mode 100755 index 0000000000000000000000000000000000000000..6469909c013eb9b752ca001694620a229f5792c7 GIT binary patch literal 537 zcmV+!0_OdRP)sI{|q~>>TZ2$qp!tjsj_dmuxH+6SkQv+K1^Dpafp!@&+{r#8W&u@@le*a|z zk$)I|{ss9HAb?mjKkZQc#wYOgBhb-*|NQ;?=l9<~e?U(E{r5MB3DgSYf*4|4g1Z=I z0R#}sfv@v<-|;cLeDMG8um8XQ{Qv#?|LOa4L zz5|hefaLFgf557NB#8Tm`R|`M3=9AP!~zPVKOloaN+E`UP5lQo8*B+s^WVQre?jpA z5I`*dz#16EB$cmd~05Jg0{{#R40Cvt10RRAw z`u+d`{`)-+^5ON`yb$y0|Ni^@{rvm=`~3g>{0I^G8ZGb(2;2e)q~Yi9|KETAVEFqN zi2na)xwe&yo%hS1uZ%yx{Q;WF%<}W+mp@!0-yWP|m)B$h2q2(_|NnsK|6hiGKudoE zUGn9{-`6*oEmQ72y~rUb^YQPmzn@?Idwh=T>wA{p3V#3shzY3p@87?_|NZ&@_cy~| zpmRZP`1kJ@JI^@8AFa{s9OeCZO+ts{j1?4b<=-Nd5zA z`2GL?FD{9%H@9*#{QvEce@o8hC9n7wpg7QIfB<3vS^!iHvfyK-U8V5ZH#le}4S|yXQB^Nt}W|`Q?5-JjKDn&hY;a$Df~n9-WqyG5z}b zHWMS`AAkU20!9tv|KD6JO#c`e|FbePFfi-*z4`Hxjg_16|9{rM|5*PsvHt$a@%k1I zzvO#v;hz8j#Q5o)BT__yf)$AVfxHBa6HpujodJ?%_y>%AV59*A7yy`5b5c`Z!JhyC N002ovPDHLkV1l?nIh6na literal 0 HcmV?d00001 diff --git a/static/flags/lr.png b/static/flags/lr.png new file mode 100755 index 0000000000000000000000000000000000000000..89a5bc7e70711575c1ee3b83cc2be7f0e1fb29c5 GIT binary patch literal 466 zcmV;@0WJQCP)2Y|A4{o z-@kwT`t|eY&mTX2eEM}Kp+i!_T03wQQEla((gZiC0cs^;{c3|jAkj>009Kl@aN){ zC);;k0UG-E&);Xo*&wq)roznr3pD=Ezu&)DC1p;}S_BY4jKAN$W)>8Nm;toyKW@Ot z#Ps&Y4S)b*xg;zq)7SR<*)x!NAa?^@4{|ZkY%l|8FPQu15397y$%U%{0*LYZ>zAxx z8}J(slm+_X#f@tK0mO1iR9wET{^!#tU}GSb{Q3uSG}s1+e?a74b~(9Y%Qpf95aZY9 zPuWDo(ENa58%O|%pI^NU5I`*FB&GkLM&}>Ys6}P~0YHEO0B+J}4VS0Fk^lez07*qo IM6N<$g3a05u>b%7 literal 0 HcmV?d00001 diff --git a/static/flags/ls.png b/static/flags/ls.png new file mode 100755 index 0000000000000000000000000000000000000000..33fdef101f74e38e2422bb85dc8a31bbf1da326b GIT binary patch literal 628 zcmV-)0*n2LP)NT^udI6UX~&u_xA`izWBNXkIq%g^6Y%Pu{8|3yZC zlYxl^Ab?nacHcU-^!@XD|Nj5|`r(zVwkHE4BV6^xC+~{3+xxCp=yQ1PA~B|JUd6PoU1p;PCnS`uh0!`Wt}y4#)fi z@&Nb%0tn=Xmv@c{N@*}KFh0M&kIP1>YRmQG4?lbdn);9N*SEXB6{h}Te)Ie9Zy;d! z#Q+chF#yj01pfa18Ye&*C_n-K{_XVnU82h1@caA!0{i;;`V*Y{7}WX#^!xhz{Qms? z{`>&^00Ic4fsvW@|G$6Ruf6C#{P_EiKfi$f`+Dz}{FL7;Z+`vz4fMjFe?Sd?fe5Gp zAb>y`UVr&E@!c&!7K){sO`Ozkh)melY+9 z5KGUl3lpbYV0iHf6xF}JF{n*t;A3C_dhHtn&^!?O1t$N2Nj?UE00RIWZBXJNY9>Gc O0000 zKY#!H`S<7dzuzGA_xCRl`Rmu;Um!M^l;`6=xPSp5fLIuQF#P%V7sv;y25A5(1xW+7 z{Q_w~Xakza@Pz>&fLMUe`uqRSpZ|Y=>VQIE8-4+ehiC(l5cdDyKm=3_5I`(zTN!l! z|Nj0Es0O49Xx6_!5M^MM5b`fj@gGk4KbtNx00a<=83P0Vn?HYFf{Xx4|Nr&tKga?w z11|FC_y0eCSvdcCFfafF5XlYnMRN&=-B{`>{W0U03nA0WvHB!TYz`r< y{$OAL2q4DW;E4VQBmbdt8IZ(*2pDGo0R{jiB6maa(%qQ=0000! literal 0 HcmV?d00001 diff --git a/static/flags/lu.png b/static/flags/lu.png new file mode 100755 index 0000000000000000000000000000000000000000..4cabba98ae70837922beadc41453b5f848f03854 GIT binary patch literal 481 zcmV<70UrK|P)?-#?r-wgj45C|ZESQtLMVW?~Zs{a4) zALIXj41fOq2a1uNeOQW%&E= z|DQh$fB%40fEE4z10q3;-;ClCKpOx8h=su~@}N>;ErcnO|V^hXG82+5i4Q+5aFU0|N&GK!5=N X;lz1sunOP500000NkvXXu0mjf*7env literal 0 HcmV?d00001 diff --git a/static/flags/lv.png b/static/flags/lv.png new file mode 100755 index 0000000000000000000000000000000000000000..49b69981085ff54568907cd51a56a1e5d8b01ada GIT binary patch literal 465 zcmV;?0WSWDP)TuF);jrk;v#5jAUY900)zv`N|Ns31p}&9s{rUUv@1OsF{`>=? z-@pHYNg(_0@82^wZ2|}&78a1v|Gz$Y3Q`J0Kshi8lm?N%fQ(=Ne*FS+xn*U6mIDM3 z3(!4({{8=rtQsf{G!?8Agn$gN2Dab7KQS->1Q6rPlP7uPKoY|E^ZWmwzrbJwx)~sVSb)*-|Mwp*NlCCVfB*i0>4ZfB zFhYR-gakS;`Tzomv6O+~6D%TsAw}vh)M$o8KMw-~K!5=Nd?C`~#DJkl4QvBtQTT<=hAW5C&pkLRXaMS_oX@ePCublh%7`S0>4^BHJ{X z&jSWU1bzAnAdrUt4F8|~c=q?-U!ZD;3Wy>I`UNEa{sJFg8g4-`_wR00IcC z0c`wlknw+DNZ2$-$7NDlT|NlUo0ap!i1F~k2r~my1Y5)izkcK~h z{{rRU9);Krw*=%9uq;py&^Z7B1lI8X4~o-~jQ<6)85pKOHYf~%iU9%$qyZ=)&LIKv z04P8aRsoF!DgoLL3cdee4gVMb0*H~J5hWs_B!uw~i3^Ex1_pot0|0+0kn{N-xWWJc N002ovPDHLkV1lkWn<4-J literal 0 HcmV?d00001 diff --git a/static/flags/ma.png b/static/flags/ma.png new file mode 100755 index 0000000000000000000000000000000000000000..f386770280b92a96a02b13032e056c3adfebfa18 GIT binary patch literal 432 zcmV;h0Z;ykP)@|4`Xj5kLT%`al?B=W5I`(ov;U*0{`Ko0*gOW1x?dnY zU=0kve*-lD1P}`lGXhluRs8wG@Eb_}{{H{>S-s!?{`@vR^^5K2FR(pO4M5WY0*DFZ zqCZG(`2G8?)UU4`zrJw%x*-cw4MBhY08Ix7Ah47E{sH^x7s!U+ztwL3`tbkv-#@=J zum1jzWCJ7ENdN%^b`n?!@|4`Xj5kLT%`al?B=W5I`(ov;U*021*0XgD3^5{teN< z@cTDV13&<=05Ky_HBiN$KMcQtBo?#b8i1w)1P}`YD=UMn?0*)P|9^oV_=9jUlG7n1 zgOt?2g9iZui1GF7*Fr)<|ABx33~>V{CZ_AxuLA@S%fEmBAbhajaRP`eP%%INfiyrk z1T_G`pFe*90tjjYTn_{=GBPrt03a7C3lKmc4KH52_yY$2zyM+rgbiXafFO_o^aD@| aAix0StzUbk+v2SN0000M*00(~<{@!wBU}0eR!0_)M!#^$%c|o1wA4mpD0t66HlA)zv9Z3HD2a;wKuKxf3 zKLhV?#{Ykr|9@us&njQ==l`GI|Ns8^_xsQFIm-b82&94GKf}}4zyJRI4@Cd}JZ511 zyk#BtzrVk|eZKww|NG~I-~WGu5Q~)bF9rsH0Ad2#1T-6>`p@704FCVWdiqyE_S>%? zzn?t&`v3p0|G$6zgQ7n`R{;bN6VP4%{xkdmsRoGvjXbc2Q*ib#i+$e?>}LD_@7KRy zzyJOE_4n5=pu>QE00u!g^X8F}1){Re7)$pq2>GzRP>hz5WF0{Q`{ z7%1`&WXbP;Kox)fFb41M1d2p!76A1_RfAN3oCFX+Kn)0M$tdvjY9_8yH^9QVjn@nfZP*{rmI(|3?O9b~d@cVAX$tK?&3V z5I~F!3@gDg2t{v?r~d;6^$&1NgV7;ZP#i-L5C8-i0C2iwRaxXp%>V!Z07*qoM6N<$ Eg62#MXaE2J literal 0 HcmV?d00001 diff --git a/static/flags/me.png b/static/flags/me.png new file mode 100755 index 0000000000000000000000000000000000000000..ac7253558ab939481a85cc06dcc4d73503afb9f0 GIT binary patch literal 448 zcmV;x0YCnUP)l$FJ^m&#tWvBA4C(n)b76qu^ z3TjKJi*u=MzAs;^(tHqT@6cZ|8AHxz+0T%zR}I9mkc`8faCz48MN^H2?$<3((yN)j;^`52o304M1f80R+|X9}Iwu*X+N)@cjDs2c+lU z?_dAGBv9wyKfl@e{~Tjr001}1@4rADVDj%Tpgtwu-=}^u z00a=o2DtG+^6#HtKYxRyQB?o^{pa7Gzs!FaUNHa!5W-0i5^f%h1nT_t=O0kRUm#-m z!vGLKP#fTS#5u+P{rv~@0nkXGhChFS;q~V)5d8o97pUPE13&;VK7Rc89~k`k^9M+( vx2Q8b0Y@nl1JFDW`UNKcfk_?)fB*vkB(P&2-J7g<00000NkvXXu0mjfGX%sy literal 0 HcmV?d00001 diff --git a/static/flags/mh.png b/static/flags/mh.png new file mode 100755 index 0000000000000000000000000000000000000000..fb523a8c39d40401b9abcfb144a73cbb2d76b286 GIT binary patch literal 628 zcmV-)0*n2LP)qpJCu1p{#SASZ=3QjbPhlO05Jg0{{#T~{M_a8tGL(@4F<^6===Qr`uzX+`vVjI z>RSuFND|!tCprH%UjG&-`Tqfgkh}r_F#yj01OWa1_W1j+!`}P+|NQ*@_xS<*{r*D) z#ao8o+xY#_%t@QS?0~yfBW+Wj{_zyJQv%*f2f%)|)v=^uaq0&4j6{l|Ybrbh`TZ`2ITPyN!~wBya;Q+wXO z|N8qsP{n^H7N*~zApZOB??0dhfB*t%@KJoPW~Y4ZyXB%oU++J<@%!g5F#%3te)iws z==lAQ0jwGr+CT&T00a=DyL0x5XB;1||6pKv0FF^K^bd&vjBEyg00RK!=O6aq+V@KU O0000p}y_ZxVsQQo9l8qD!tQ%&&F2zEbEdU-v3mY$p-gb*;wwp?LFG<9EeNpZLj7Q z>zeacpNZ>XJG@0bcmXcALo;Ad(L@#C92p0~G#aM!FfF0T7^YIJVVFaIl|0gRpSyF_ z@0dgJ{oT}qqUk#4;-a-U9Fej5EJ`tIE9)E!qDfL@GEq>vEDI}q@P8EmenIHxu!*Cc zLK?@%1j7u0A`mPlm`kyT;5daWs;EH!+LV0IWO0Zyj4+sHxRqia#e9ki#cu?^6YQn9 zOfZ8&4uw%r9nR_}FG?tJ1sBpnAsdGMh7pA$Mlm3ABvgz9aj&GrxaT8{1^YB+UzPEM zQQ5*0;(QnblJRNh>E*%16wccXl466KPu9Lk=M%$}%9~Z3xs9na6KbZ+^U;AoQ+JVg=BO3kgY$Vuu?iP6r(sQV=EH;Iwf|hcN2Nd zPl_EfW;$kS3zh=H4ojC&X!7Bd!`~OdX{VxLp z5dC3f{KL%rhe`hT|Cm4jfB*jf2Sk3^vlaWpqpU2@27mx!VE|eEAE^2d5dHlPWd8sE>;L~hU=6?i|N8~f1J=Os z`!`SnKmdWv|I6_A575egAcQa*n+Bj_fB<3vdK75(@4vtQ{Do`yh0yRHtQw*LD9^wE z5I`Uef5B$|`u7W{2T22%M6!Vq>?D8y0%`dD|M#!IAl1MB{`!Yz!@u8fUYwuxex*z#9GlH2{4I3~mO10Ac~g+V6jVB)KI)-uUyEi4o|t-;5vX%A}^sE-TCS?;peW@7xO)zGwLT z>;Lb2Aa%cf{|B;v0lB{!Wo3aj00dxJ4sHN|VGstA{YR$4C2D_2_!cBcz(8l;%*ju; z_5-pDt^fjRVEX=(;rPk#zkV}({q}dw+K<0~Gco-B2UY|%8?FIpIzRw{{P+Lo&pY>j zzJB!i)2Bb*-v4F%#qjU=2Y>(qX<+*Ohe1*jY|LMVKOmiqU?UkIBoO`m3qe4|00G4K6*VFmu*EK13J3rK Z3;;9iRuWt9^;rM_002ovPDHLkV1h$@)rkNA literal 0 HcmV?d00001 diff --git a/static/flags/mn.png b/static/flags/mn.png new file mode 100755 index 0000000000000000000000000000000000000000..9396355db45a8ee040c790782209868acaad4b85 GIT binary patch literal 492 zcmV@{}>pU8D26lh`@M2^zAEy;6E4#hyVhJ1te8n&0xjw|I|OmAOC9q@qGTx`1kMs zKYtki{9$JJ&B6L7=Kt^CKt7QCvS|}Q0D(0y{Qoccl;O=^hIfA-|M~k7Bn3tPfBpOS z`}Y4|zknP@Sy`YB00G1VQttboLEtY##9yH3?>`J++5f+PL6rRiQNLgu1_potVgX{t z{eKx$|NOuHhk@zOpC3@OA=>```VZI207MK7009KDfg$woe}>=xgMa@Q`TZMV99$1f z=+7UZ=>P!)^22||KmS$!{Ac;a@cR$YT8L7JY6c`57{N{g2q3VN{s9Bw7X!l|paf9r z=TC&I7$7zP9Rm!dKY#u(FaQJ)NCWfVKmVWoW?%y` zm}dTA_#*kA1?Z>0QqoLZAnySq0pv->KTM2<4My5DUW>hHBSpkmCRUfavf4zkmMxWn=;={POe9 zpZ~vq|NHajABg<@_xGlmn*aic1*nOE;s2AbPk~B-;NSn>+$?|p{{I~>&N%tu*Drs5 zfBO0B*Z*HY)v`RYKsx~fhzW>+Cjb5S_s>6&>Le+qufKnBv&bF%YWVBlABO)w3uGC7 z{rLq%Ks`WL0R#{e(EPs)fB*akI^_5N|9=)g|N8#t9~WuY61}otf4Tqs`!(tD7m$X( zzkdG%X#fZyCZKPCW&?Et`7z=QS^~DI8Yx-=nhgK{7whGvYgWDg!B8&G1k~{7FHk81 zKmY(S0M7pd06qXVA~x>%?)v`v$?y7WENVL!I|c#<=Jn<;5-<%03=swpg4MkH|N9RH z59P$=0*D2u3CIB%01Ag+CC5rQSvY_G`T70NcNRvLcR${}{{9+hIZ*cRKadRo0R++j zv0Gk5I)>DQ+oULEUYSww-@{VeP#&z`*?j4i%tcfB<3v#u_k;CAlSmszIWE zes6rZ@!!9HKqG(r{Q)GE1e8GG4GeUkhF=T-0mR5q%E0gm7LmVTk@^EErhg$tKMw;y afB^u%K|axUkwLit0000IqP)p`2X)eQ1zd`|Kz0p8A$%R z`bTZnZKVTj3KfeK`2Ky*5B&e@&)?sFfZ#7s13&;VG5q=W=RZg_5SU54z5Mv&imk%` z|FW?#NF2HHZpKX|PKHOnfBpIO`|qDW|9~0*0*DFdzkh#$Hv9ux`j_+Xe->2}ZC)XM zHV0!dKPS0Ur}$Y}|Nr_6wBa96o`K;HKmdVl_zUt7(16dMgtTSeTz>rQ?Z?ko?mjsC zklD%d+V79-zd^=BZ2$-$7GT%`Z2%eY=kMmT9MVkp=Y%mFes6g9+4CZ|=Y~?>`*tz_ z9Sja7kOqJN0yzojravHW{k?SeQ|&okK5=fKkARLe6n%c?uEf?;0ubZ>f;9XA2p|@Q zU%!CHiwJW41sn7GB}e}J|9v$?qW1mmH(Bw2pdk4RbO%twKY##YWMEE2jz}>2 jgG53Y5Cqh~01#jRFwa04;J&RL00000NkvXXu0mjf4K^ZQ literal 0 HcmV?d00001 diff --git a/static/flags/mq.png b/static/flags/mq.png new file mode 100755 index 0000000000000000000000000000000000000000..010143b3867f21e7791b8254e806b325c13b2895 GIT binary patch literal 655 zcmV;A0&x9_P)$g8u&g1qF5h0Q6aJN-{1Mbz`Ie0O|k$ z{sM@ZfzzR@(T$Ir&B098+eukMUeMl2+1^x^fq~&CAE%jZ{^O4ipMU-bgNM&P{`mQajg9%ivrm#DT>t+4XJGgP z5I`&pzyAFG^Do#{Ra%^vg_(hwneqG2-w+MozW@IH=Wj)-86P+6=Wo9l8G*Kb0|+3- z*{jc+=}JF+`H7W<>HmKQUT#*P1{)(8poaY?9|3*x^4(W97N$Rc|4EDS&R=&5Ab=SE z{{73y$Ox4F`Rgyx2o@H`|EK_{=f}^#%*+g|tSlctd;ka_mU(L~nd(ak@UpY9GCq0n z(2rN05Jg0{{a96b$W_S@ACL(Rxa}M z`G=gy|Nj54lTiQv|3YG^`}+MH7z}h_G^4TA+~V=RqI&{}iQ&&bpkF?J{dw&CQ*JI+ z5dlsHn4|vw{rBhJzZb8+p1k+fF;B_XE! z42<9q0s8Xa-+!garV_&3Uw`~!Vgv;%KmY+HvKbgYfiyF)F);jMVE6!x-Ip&{0DU4U p=>-f&1_my$6wo1$fFwYG0RX)13*@;vt7rfK002ovPDHLkV1lt{Hh%yB literal 0 HcmV?d00001 diff --git a/static/flags/mr.png b/static/flags/mr.png new file mode 100755 index 0000000000000000000000000000000000000000..319546b100864f32c26f29b54b87fe1aee73af21 GIT binary patch literal 569 zcmV-90>=G`P)rBb0vs`uywH-=91j zzd+y*NDt6q00G1Vbl+cwzkmM!`SbO+m+YTk|9<^u`R%^_m*bXSzgT|#{QE0P`4>nh zQ2ZZA13&;V0loO=-(MgHX#3gMzn3`u{`KzHucyC$Kl}CT<*&8wzt6n|ss8o%*Pp+C zfTjZk5DUl#p#T2<`NjV0^vyqym4EMy|26&E??3;3WoiC?{O#A4)4zWSfZPMI0U&@t z8bCe+8UPFj-d`E(e|zZv%GLeN@c(z$rC;+8f>rHtN0D&|BIS>OtI{AOC zx$tZGDUcCBkzc|7XAeyp!)#=hy@sHz%Z8Nmi!G71?uGb{rk^vkcEsO8W!D1Ad0T=%Vj%gtVfB*vk>3V2g53(}_00000NkvXX Hu0mjfpCtxQ literal 0 HcmV?d00001 diff --git a/static/flags/ms.png b/static/flags/ms.png new file mode 100755 index 0000000000000000000000000000000000000000..d4cbb433d8f9fe49f06585dc46ee15593e3e621c GIT binary patch literal 614 zcmV-s0-61ZP)w!6%f1G{My^>{rmp(`~Up={s8&+{Qm#<{R63-jRF8M0M7pe zp74|h+7SfEy9M#{{`U3;5)b(M0y+2j#`QMv`vWF8IQ;$o{QUp?{QmLR&;S7V0*Gk~ zkHv3!H5OU9Kb*Y(q-ELEH9vC;U*%N({+r?VuV26a{F4-VqagC^H&EN}zd$De1Q633 zj~FHafsLPJA96}RV-#A)X~v?X^MOAye@1K9u&05Jg0{{zeC zH7h(V@9+2F_Xpti2mAc}=j!&)<^S{h0sZ;_Q7ob~2($C-{v8Cq(%>lF*}4LV2^e-j zV}AbzqQAd@=ogUu2Vs2v^{02G<;{1(|2aQ(EV23Z2N=G800M{wXv^O}|A7VrH9+)$ z7=NG)mcRcxm)QV402KN07wDuv009Kl@CPU%A;t|Pff9c}I{*Fo2PXf3L>PYm|M&kd z6T{yxzy1Ri0|XEw14AS%MzNxQNDQDd27mwq0OfZ^Ej7^!+W-In07*qoM6N<$g6Gg9 AMF0Q* literal 0 HcmV?d00001 diff --git a/static/flags/mt.png b/static/flags/mt.png new file mode 100755 index 0000000000000000000000000000000000000000..00af94871de66cd0fbf0ca8e46dc436d66e2f713 GIT binary patch literal 420 zcmV;V0bBlwP)Io>l+&W2PtRx|M&0z2M_*#`0)SN z@BhDl{r~j~sG3n$7H9)N0I@K!uxMy#e*W?mp&AJO{R1fisrv=e1J=Os`!`SnKmf7) z1AD;K%nYO&458rnZ>;h=xCZfJy-Z2&4h30>ylo1|%C8 z!A=4QAQtS#|N9SD2R9xVP=6R000Ic%Boqz*|DhQF2dLpM!{5I^H2?v`f~*=Iag5B& z3^Fndyu3hzLFn&a2m+c75I`Us-n@D94+enNf~Z%o__l08i(?1?2rvLmwOi|Xk;8TX O0000wT1SlAdCl&Y)$gVg>5qW^#Y{{Qp$|KC6V{`~n5M8AIn z$$wxH$UeVm6F>m5>|$VWV&MJy|4IE<4N06-W9 zfN4+=|Nq*iiwn$Q2C&jbT&$=TD3oCSlHOGFpRx(;BY;>KBo9alsWSh04>T2o{{H<9 zq`@}){__Va_~#GM1}QP2gACgN0*LYNU!Z^U|NnpRpWzq7|3Cl#|Nj5~7gmyqN$LIj zQvdWzhA$B4g=Z%5I`U&fgJtk z_g|1x{{H;?U6oxK=&at?J**6@V8K5Oe}Dh~1#%?NbbtT?*#Hy-y7l+}Kff9N{$pgg z_WK6Ie}+7v0!uEdpa1{-Wc>4s0pu$NunhnK1op#kuysJ&8U8Z;`FH#0jm^LQ>Tv2E z__*)izhD3U{sq|pR0FmFAb=oF`osMFS8?ijP~d>v`s?4X6Tl#928Ghke_*>=zWoA* zI8ZS_0I~c5+3<%^Qj!Z4rNibE_6+kRtZU3J46>yw zfWS`r^B2emYGC;Nhw;yEHn21ZK^Q<1%m5k+3<-b$0%`dF7h*inus>j>Kov-;VIf zD*pWek$*v<0SxiK|NjEzfQkVEh>@X`f#DO--wX`DVCWAL;}4AS4@`0~00bBSj-y@M TF2~k{00000NkvXXu0mjf^ET$> literal 0 HcmV?d00001 diff --git a/static/flags/mx.png b/static/flags/mx.png new file mode 100755 index 0000000000000000000000000000000000000000..5bc58ab3e3552b74d990d28a0f500e9eb6209dfe GIT binary patch literal 574 zcmV-E0>S->P)LFc1LT4cKmY-iGBrkbGB7ay`}g

Nzrc?B z!}RAj&|v@p#KiFL&)@$*g@6D2{reke@BjagZ=QH_|HA5tt#{7v1Ul>guV4Ru|Ni&u z_rKqNfHnXG5DUYvUm)ZE{sF26>H%tC6A=5t#K$G4Eg-G_A87rbKVS#^0WyF#00a;d z1Ca3#WHt~1jRI=eclPL04k1AU{avRH{sTG*Y{UOQAO!R;KmY+XFaVtnvEdic(+vNe zOr8IIc?-17*~$Z`|L?Eg|G`fB4^+g!01!YxCouq>54Pd=FQ6a({TC7tjr5F#2GXxz z|3N{=01TJ^fBykB00a<7!|#88#Ce2)8ovGh$;ikEv=>5x7)(s8a literal 0 HcmV?d00001 diff --git a/static/flags/my.png b/static/flags/my.png new file mode 100755 index 0000000000000000000000000000000000000000..9034cbab2c02704b65fba6ecc4a7a1c1d053b6c5 GIT binary patch literal 571 zcmV-B0>u4^P)Z-xKO+5@Bcl`=5c~|G$6#Vc^TNX8-{JF#yj01a@w+002>mTdm0Dxpa1{C#)An)W@eTA0)PNwiNI_f1h_u_DTrb&+z}xv;{Wj zFGj0Cgf$G002ov JPDHLkV1gu%1+M@A literal 0 HcmV?d00001 diff --git a/static/flags/mz.png b/static/flags/mz.png new file mode 100755 index 0000000000000000000000000000000000000000..76405e063d43f2f3b5b9cae4f76d9f1c73cea25b GIT binary patch literal 584 zcmV-O0=NB%P)d!0?ZO;SY>NM!#Sr3j;s^4&s;y01$=&U~zyG1R{bN8&$-J2AZUB z{O~2s*67}BSLujJh!l{(TM_&V!SLx<0I@JIFetqFAHVYHe9OP56#xDPD){#oDEIpx z82$bI>))?me}4fPvfQ#j8vp_bq~ZU+|9p&pvv&RstuJJil>ZL~|9}umff)b({bONa zxh{1bAb?oDF)-ZV`m5;k=ljw>l6BpeezVN}{rlPPKR~N~|N8X{NCKH4>bI<%?516t z00M{!=o3jd#=5m)yzyNB8Nca#eH--U&Fe3(f#?+keR}os(@P+F`}Hl*IRF8~0t}ng zTmN>%etWlrgX7)b6MtD}{$cp_8-#v?7>pqD50GU3{f7;x0U&@Fe=;yIS7rUbaG!zu z&%cJ+j{>}Z{{3SByBg&1|Nj_3~Dr2e;Gh>zZrofD5`!lF#Z7{ z2B7#~hM#}`y~Cfd!0!j9&~4{}>qlfDw=_GI6Spo7yiaSL1(R34j0qF#yj00RRApetkm@ z3-<^I`04Bn_xS?+{Qmv@{`~y?GBy}bOB@O2-v*J4|MK+z`}_2=vH}QbDjOT??*y+^ z9PHma=D0k1DE9l$Z%$6;=mbqObq>D1hX4By|NHgp|L;GHwRPtz;{gH)sDXiz;U6Qz zZ)V2dKoY20MNK$ASA+NcQ;z)f|Igq2|MmO7w9@DL!aL%O?v>U60R+_W>(dw3rWqbw zY+vs`7YYhj_p}$}TRP+a`c40T|N0L!J1pWwjPJac>kgmX!|;RwAb@}x{`~&)@5xJE z5fO>bZXLnj?>WlS|KEEGbn$;~p|4%Vx0QJ+&gI^Ic|%b^5f~5v0R*z)@87>RPSSB9 z(mdPNGA>^D|K~4|Gn`ysXGCuKcCvq8=a*k!egj?m?>8{m00Ic8fvd1cUx@iTXM5HE z^H)FyGjo2eEq-FG+;YA1)Puvn7=8hj{`vO@7;-=x00M~R?)3!82NxK-n*V?K1yZeJ z{HiGaByR2;1J+usHUNMy48Q{FH!M8N)Pwp9Y)wTFo81A^+}-HTA|tp~ zO4-IyLWKVGUAVv#KrFzpU^vb8&*4vW%&*_Sfp-5+Oa#jR{(FIy5oqwQKVTa`BqPut z3=9AP#KLU;d%N^MnR|>Uj{MoN{TEQv&!4|iQhqnS=35v13ux)DKYxDz{qyJVA7#|2T literal 0 HcmV?d00001 diff --git a/static/flags/ne.png b/static/flags/ne.png new file mode 100755 index 0000000000000000000000000000000000000000..d85f424f38da0678471ef4b3dc697675118bc7e0 GIT binary patch literal 537 zcmV+!0_OdRP)pTI(!tjlO;rIXl@1HV&)cpmDGW-GR z`3+L^2aNvy`v2$G|KGnEMPz|W00M~d>&=s#l8OvKbwEpiHvESG2>tK>|GyyO9|$ut zvpzk42q1u17-V!9I61+pfvQ08|BZwH@16Yr=jVTUlmA*S|6zcEk@1g`6+i&7`~&$E zYA%%V^7j8zTmS$3{hyirzos*?2B2bq00L=%yMh7Y5+V8jy0-s+|NJkm_8$o_f}Hjb zAb?n|yt|^zto-ZmuYW+t0)q+&<<KJ>BgrQT zRQ>1Q-#;J~AoS`6pHRCwBA zU~p!DquG0BzkdJv^XJbWKYo1u_VxSs@1MSW+P$@#L70Jomw|zm0SEvBhy{qNbE|>m zzkmO1v}^=;1(+C_zx@3C{m*wPUMbGM98Q`}5v}3B|NZ&>_xGmUO#lG|(!jv*|H=EO zfB*gc{`33)|Nj{n|Mza|{r>BFYe?(AfB!%K_3?Y%L>Z^l>!7{V2qIffI$#e zz5f4~?wt{_jNwA=k=tvV8m;04hxH1ClNsp|jaD0d_yUND;Sa;#|9^ps{`~n3bQJ?5 z!?D}Pp1*tk?A@~oJ0>vvX8@W7)C|%C)&LMdARGSt{R`y$`S<5P0|@TEy!-p_?>~S2 z1lj|1A`hhCuT1`^U1^@yK06yzfAV)~#?EnA(07*qoM6N<$f^cITh5!Hn literal 0 HcmV?d00001 diff --git a/static/flags/ng.png b/static/flags/ng.png new file mode 100755 index 0000000000000000000000000000000000000000..3eea2e020756c41abf81f765659a864c174f89db GIT binary patch literal 482 zcmV<80UiE{P)E-@kt#fq(zr{dlL|rVbPb$^K$sU||3VAQpx%4ArjH z5b1ya|A`BUvomopF#HDspdkBi_LzW}KMcQr|NHa%@9#}BHvt3?3s4hK`pMU)fB*dj z0+88E|KR{Ac>Ck+U;lpn{`Xs!M;2%gKmf4-G0FZ zKmdWv|I6_A&;LJv{(%shYM_N64Szv401;3FKmf4-Jp?oxsQb@fum%PU8~*%)I0>i# zs2CuCKpOsn%?64Bg>X9QKhy?*00Lh)xZD#`v33$e!wuw~|G$AI0?h>~ z1_&UqlR!#;Ll9OQ{s1-n2ZahyF+c#Z0Amdp#**BUAaDHn%gD$G3dPzV3};X@Gl13bFaQJ? Y0MmSSAW;3b&Hw-a07*qoM6N<$g7#LQDb&XuwlP!fzJKM z`1`}(|Np-K;|B=>MS!;cWf13KY+LvSAb?md?)rCHn)&`aU^p;VI>1V00D#b-){yc34R8!1Sqb680000@|4`Xj5kLTv#?55wQzKoX38|NjLffBpXj6#2y{D-E;(Ab?mHL=FGT z$TP6>fK>na1GWK({(upf0nrSU5*Gz(00%w4e}91F-@gjtJbOE500a;V!?RECzy5mt@dro_q~YJM-#~SLe}OeX zNT7y4|NilCvNABd0|+1%hF>6clER!2lYo}|1|zVkK=SXOfB*mf{rC6ZKVbL)odgg- vjKFAPVE6?_e~=h|kVO7~qZmRm0R$KTYszPUy89K;00000NkvXXu0mjfB$dbi literal 0 HcmV?d00001 diff --git a/static/flags/no.png b/static/flags/no.png new file mode 100755 index 0000000000000000000000000000000000000000..160b6b5b79db15e623fa55e5774e5d160b933180 GIT binary patch literal 512 zcmV+b0{{JqP)O=a{vExMP2%`MCSoB^FIcLe_%lf;|~%E5I`(IQNh}3Ao>6Q|DFUXMn*>AqQd`w z|1kXd^B;tM|Njjl{{hM0zwd6?1Q0+hV1xeud-4=Wy?p-%sO`^#2S61Jzk!N?s)6X& zzhA%p|N6}=D+{y%Ab`Lc{sL9~1=0UN4*CD*7s%9KAf+JHKs~=eB-8KTKvw|-5R1&; zzd&a|ob(5%^Z$Q=wHy9p13+aOpFRNu5F>N&`Tk_-7w>=n{RejQzkfh&Kn{rf10?_b z{tFTZibx5v&dxav5I~H7|Ney-|DWN1$%1FyagzUW0464;_wU~W1Q5$TW@eGxtUvee z3vAf*8|igK9~@*rr66bh|NrkNM8z+V zAV?>O@ek;bKfu6d00KAO>U2WB)$@6+CBT{3ok>^7H@y{~6FRKmalQ{r8#S z4^Xx6Je&%q%J2@E@iKAb=Rv<(vO+e^|@#56B5{*~0Mg z>UitA{$OPI`~Tk`F#7!u z%=q{3H<BYX7n!%ts-g52=?&#%8AHvnDo=g*(t zU{@(f3Lcro01!Y-3_uV4{%5PNloM>zp5gQNALI8Qf4=|z|KsPsAHV+n`1$wy&wt;4 zfzU5-=mP{0FqD1)U3&iJ)0b~Q-+l7x$B&=CVSxxV`w!S3U{3%6(9;Y60R+_W0~jFx z85t(6Ir{$dcL`zMKcF!E2U7}WK$QOlX#fZyMg|5;)aZo7D4hMD0TloQ7ytwk}@P)zKX@%&%DST%gMqDF&nJ<9}^SM9e)7=i1G97Xij+!pc8>s z0}TKw06GH{9{>J;c@WiqflO8wrXNp!0t65X1H*raF>uuoM}q1_&UQ zzyE+PV}l7nR6|1@Xe}f}K_mmn5Fh~%KtLN9K+(a>4Du5*FzA2|{m;zE0Q4slD+}03 zKxJTefLQ+k0*D1@+|Oq}Kt_TL2e}yHXa<;L{(_vY&wh05Jg0 z{{*4}t^?F62l4F%@9_Wk_yrLV`1}4n_xiEzG57obO$p>A_3H5V5SZl@9UBk;0Q>>~ zF#yj00jE58tx;oDUfVAO7ZA^g)(IQ}Y8G-;ZB^s|2r!a55}@Ef&J{K=uDyPxdE3 z4FCZEF#yj011R$m=m{0X((d>7`QGgNy~*X%==~K4{m|+BO$p;F|LeE!5zX@o6aMwK z|0Cnry8?)1@m-dDW`RF^3@85Z{`vj)H~*iTuYX^D_V?HC|LG63e*OOQ`}ZH9+h%{4 zR^kLX2_S%&fc^l6#;?EsK&pTL{q^hL&)-1T|AVLlN&WsC!*d@f`}fa3pay^dVgYLS z^XKpHUw?lCNw6Z2x?f-t$o}(Zg7`(je_wy{{yg~C5U2qlfPfml0Rvc4oChck)CmlD zkWL^2sNoMV9{$c|wex0wa`2z{-@pGE82$nT5F-OaFayIEq`3SAj@n=l%a5DE9w9oI(Q6o;?Ey zAdrT?A3yTg*!;YE_t&FGSk*xQBO~LVKYsuMhzTgoEGG70+cq{OrGLMF<5CR)KqmnN z5F^kJDrIFqfoA{t^Z)mszkh!J|NR@N^WQHZ-f!>?ZqK&!y&{zFIzP0wn(b{R37FH0BS}Z;-(ZzkW0R z`o;JgO8$qifBy!003d+C8bA*C^$+NzU%!${e*gOW>-S%fvmgwhvw&=%Tb23#oNZ?S z2p~`>{R0^fayL*XP!U2MkOb-c1#;`ZzkmMzW%$Pc5I_(c{;*0)g6#S8SB49$^)FDx zACSQydx0wd{{073{+Hno13&;Vu4Mp{AoLGRCWbOF`~tJSfrb9TxDaUu27mwq0KDvZ UcsT?Vy#N3J07*qoM6N<$f`X&bC;$Ke literal 0 HcmV?d00001 diff --git a/static/flags/pa.png b/static/flags/pa.png new file mode 100755 index 0000000000000000000000000000000000000000..9b2ee9a780955566cc7dc2f59ce175f32d3731a0 GIT binary patch literal 519 zcmV+i0{H!jP)|lVPPN>g8sl5Km-s#EI@faK0YA%?%RKmGG2!N|AD;!zX}Teef|3X z_wWCI{=muCn>GOi5DUcMXP^GBIsf;|&;M~|Oj3M||NsC0d;h_|&!7JP`u*?sum8V( z0aY`}$^xwd2q2IKptK|}gRT_Am!FIf)j$AI2PS_*H2ec11_potVu8EfUYUvEKf`}G z`1lE^`X5*+Sm$r%-@k!200a<=Rv$}~=bu-f|Ns5_|IfewfBrH6Nrpdvs+^pde*a>f6qSrfAZn~Z?Fa+2||AuS*t4lqXH%-ra!^K00G3r z@ax~7e+)2nAoSxWko^y??$?YNza~t8coi7j009Kl08{<<|1U847wF+XP}P60U;n*# z@9*{N{$9Nb{(u|?WHSN*&{SqkP0pSkP;|=6L&63i zfEXEE-vV95@Z%rDuRkCLl>P7efnPxL{(%8d!yj-G00Mvj0{}mHT?%@XEt>!U002ov JPDHLkV1lK4=}rIu literal 0 HcmV?d00001 diff --git a/static/flags/pe.png b/static/flags/pe.png new file mode 100755 index 0000000000000000000000000000000000000000..62a04977fb2b29b96d01ffef3b88b6bf2ff05862 GIT binary patch literal 397 zcmV;80doF{P)@|A6@UbB2E)`X3x9D*gjYK@dOyv49j-R|ARv|Nb!om5Pi12buAo@&A8j1_q}8 z|NqCt{Qv#?|DQiV^2?@8009Kn!0`Y7lcx-S{{qo}h;n3b`}Y4|zksS4Wo3aj00a;V z*zEsM)xUqEs|Es~YOn@|-@kzx00M{wh#7&Zfj0j6!|?mhe@uX+0cadR0D+zK=P$&S z1T_5l12i2VfWS`r`v>f&U-&gJf}I2qKwu{UjsNuvF#Z0+ASnqp<}V}$85tQ7(W#^a2}2+PDh3E3#;-`R r2#VqJ=b;e{^dbb<+Crtk03g5s0zF}bJ8sS=00000NkvXXu0mjfKVzU% literal 0 HcmV?d00001 diff --git a/static/flags/pf.png b/static/flags/pf.png new file mode 100755 index 0000000000000000000000000000000000000000..771a0f652254b4e891fc73910aab38967864da54 GIT binary patch literal 498 zcmV3lobsI zohi_A`bB&J!~)dH$ngL7lczwX3_#UDxxarw>LBRX?|;94{rmL`$Yzn1{l&ll5I~Ht zU%nI;6$RPw9|VBf|1(_vbYnFmA3K-0+yDOt{~_StKSoBzkDopP1Q63dnCt(82%zd$ zpFb}6I_2!oo##J&nDhVtbEpQW0tSEpVuGrMivIupee<8UpWZS`Uj1hH_v_am-&g;K z1CTfW0R#{WvT7jw$rQ2Wy6P|4+kZd)xq2}(*g=aGrk)Yxu73al#Db(Bq?4D0-N;b? z5G()RfB&pB<@s4TkY)e;`2!F@EcYcO{->q=ymt>64xqsK^^5b@FIO-F$h{9?`~e2* zUv?FhqZcm%1P~}#|Nj2NBq_=8`#0mCzd$+0-@loGnqiRuWPl>)F9-k?0|XG`aR!E8 ou!#JF#Q1|6-w+1S#{dBa0Kx%7Vg$%BF8}}l07*qoM6N<$g2}Akn*aa+ literal 0 HcmV?d00001 diff --git a/static/flags/pg.png b/static/flags/pg.png new file mode 100755 index 0000000000000000000000000000000000000000..10d6233496c10e52ead975c5a504459fad68ffb8 GIT binary patch literal 593 zcmV-X0Hv=@BjaQ|F{1AU+wpQ{$Kys{`!CTH>0fVKL!SX0Ad1a`1cQJ=-<7ptZmH9SAnX3 z{Q_yQ`~BbL*ME+m|K)!Dcl`DLKS&U$0U&@t8vg$M`{&P}#F!W*N5{%vzvln`{r&f^ z{||ospYrSf)?fc0{rZ3A*Z(w-2B2bq00L=XVPVP4%$zZO`i8aZ($dmav$E#>{B`Z@ zum6{R{h$5&|A}A!9e(}a_zUO_pbY>4IGm#s06-9cg3a4XeYH!D;D>_*`V5R3;NTtr z%*@^Mq=>M$LXVh`5jChqDeaf800L?F_U+sH_3M8E<^KOyQ&ao$p literal 0 HcmV?d00001 diff --git a/static/flags/ph.png b/static/flags/ph.png new file mode 100755 index 0000000000000000000000000000000000000000..b89e15935d9daf25173f89a36d8111824fda5db5 GIT binary patch literal 538 zcmV+#0_FXQP)N~0_Y@w0Al+04@f;`V3_{@|8#9F=K6XQ9UY*azdwKd1Cc)=8bAo50U&@_7#Kb? zFg*PK|H1$NTnr5Nxw#FRn^&xvtbK0f`#-;cuKxpc)t_H~fDExeq6ZmH0|XFI1NZ;` zGXMXp{AUnj_^>}zDz4B$`$1}#7U0X2MOU=aELU!7sY=Kr&1{QvMCSsj$|2dLpM12Fg)7ytqY zsKMy}yX6c^FZ@4woIyZ63`(G)I*8l(j07*qoM6N<$f~Z~XumAu6 literal 0 HcmV?d00001 diff --git a/static/flags/pk.png b/static/flags/pk.png new file mode 100755 index 0000000000000000000000000000000000000000..e9df70ca4d63a979e6bcea2399263c081ce5eaeb GIT binary patch literal 569 zcmV-90>=G`P)`{xe>ko^7oADI06=l8$gn`UeR2p|@S@&Et-|M&0Ts{jIsi2(+HrUEqpC9EW^)CAOaJl^^9-%lBC8BHP081)#ChQGgo zHUI<=3(zGH+B`^Tu)Z%{q{PpYS&%;j+KluCr=pK*_e;5D)h=t+bKO_wx|Lu6N(?HnJ zN6`lu;-7zgR^V0GaA(7wCwqWF2h;!%KrH_l5Kj8_?>EpHKuhXR);;uSzaAK9KY#yZW@T0p zQ2G1+FVF^n0AhUp{`o&J`0?k5YKbbt8;l4>j(9ExfB*vkhsr>Vq>*li00000NkvXX Hu0mjfu=^7c literal 0 HcmV?d00001 diff --git a/static/flags/pl.png b/static/flags/pl.png new file mode 100755 index 0000000000000000000000000000000000000000..d413d010b5b097c4e0a4604eba86dad79567ed16 GIT binary patch literal 374 zcmV-+0g3*JP)Ab?mv3iQ8Nu5A{|6C_|Nk>HFfjc`0biay0|+1% z2B@O{$c+Ek03##gzdwHf0*Hl3)etG5lK=vU1?W~rxN0!OqXDP`Ab?mH`1u+7`u;zA z_W#cxhChG)!_n{G|9=A+zrl>(AX#bYe+w4^1Q6Is4VEzI_51&? zUm!LT$@u9L&^Z7B1a=Zw2xL66B2?87%l|Mi00a<712fnG3~)u5+Wr7F{AKw27w8;- z00L=X`u&GNQW9*;Uxq&*os3{386YGO{rw9;K*az7#P}5)xp2gQ%0-g`0)PMm0MPYZ UsK>Njp#T5?07*qoM6N<$g4HXSwg3PC literal 0 HcmV?d00001 diff --git a/static/flags/pm.png b/static/flags/pm.png new file mode 100755 index 0000000000000000000000000000000000000000..ba91d2c7a0de26e554979f6351d42a1a4e22de3b GIT binary patch literal 689 zcmV;i0#5yjP)#D@XkmAv!xDMMW8-mEZ&mK%|%B2@pa*J{pUD?(_ahBqlG}*TVt; zF#yj00{;H~{{a8``T70x^5Ww5-{JLFN2dS#-{Io#@$TjE?&8zZ_V@JO>E`A8`2YX_ z`~rx{;oS`3$G61pUY_{!-0$E2{{CbX;QPSJ^3BEStfg-V`n;-q6+;t+Xyw zOy)sS^8$#8OF-b~@87@v{bTs||L?z_Zq~=&zGIaZd&b4_{m<{;|Ns5^`sL5xKflEU zUjXHSP67x3F#yj01nBVe84eEr00I90|NZ{}FelkBB-0BC^O2xB85HWz)f)HsDi#s! zMLEolpK<^Iq5=Rh0M7pb=>Px;h=2d#;^FNBZtPGJ5bKP1n*{>+?(qio_5k|){tF25 z`1Sjqtv%A&PR-7P0st`p&i?}b008M5=kFQg|M&R&{r=?U4x6PnhWa{`B$w z?CApk{rU?E^8f<&008;|hy@rKe}6M^a>=kt2+9et{QJXr>zQ5ua`W#$8JQS={rbi5 z^Ctr{)6XBj7@3%V17i>%fEb@MFnj<<;{gVSbOv{z8Q{qN1r~!u@ISB;HgNQF0R$KT X>6#2<48ze<00000NkvXXu0mjf$fQr4 literal 0 HcmV?d00001 diff --git a/static/flags/pn.png b/static/flags/pn.png new file mode 100755 index 0000000000000000000000000000000000000000..aa9344f575bc92f4c1a5043e6e7d0a8b239daa64 GIT binary patch literal 657 zcmV;C0&e|@P)$VEDzr@CQtS7ytnPF#yj0 z0a0(~eP|Fd7x&oU`XvhO+V>M05dgm54eRCe)!y3s`vCp^_5Az)`uqj_{QRhrlmY-T z0M7pd#W4OILofm6?eh2PFCO>E*7Xbx0@lgN z0st`p&i?}F01o{F1pxvB0QdR->g@pe`}O_>?Enew{R9F2_ajBO3l7@{2K78Gz9u`z z008^~05Jg0{{!Vds4FN77!dx<`5YAi^1B#F4D8&$5I)oT72fOU?&Jm;1k~m5*82ws z?b$fe$;$!&F#yj01PbEP9OVUy$0!>9?*iHF&;9=9{2wXIjNS}D{P+g={1WdM4-6DF z>gCe?_5uI^`T_tk0M7per>)2uHb&;=(#87${QNQl(a`kw{Py}Y1nctu{`UwC>ihis z2n-DS{QU(1_5c9-0*DC|aR2`P{`;Tl?{DV6zkmJy$H?%Xjq&fFKfk|zXRCbma58?r#f z00G1Tv;`<3F2eQa&)+|P|NZ&>@AqGj(m#Ly0)zk56?XydpHmj-@bfY4J_0oTA3y*x rGBBhgM-@1KAF z|M~a-&+mVKfs8*8^zZj?AObQ@tXl^VK#U9w|7)um9zFT5!1JoGnu}M2{r|uJ|AC@F z;y(;9F@d=M{=L6-3m|})KsNsS&&$bh^PO0375Ag3EPt6;7?_!X=sz3*^#j#0Gyh{@ z0SF+Vlm3Z`F-Ar(?BDbM+y#c5od5B%>y$nn=KcNZ|L>nr$AC#T8JS}%RsaMLPy>UR z34^BAe{Qb-hmSD)`~82~jewZHH;sOtVgB{~2T_Lx z^M7~ue_6TzObGu%0nl7VM#g8?t^ouP$PYjMFazDG!uzbThlgL98zlelKgeJZ;~z*Z zSPxJG!?$k$0R+;(`ulr=6P)M00|Ni~>`!`VeZ#eh`fLE8Q){QmQQ=eGY$|NqCtU}^v=1iBd@fS3^8`SJ7r@8ADF ze*C{^(f_{w|JZZ@y#o+HED**2KY#i6{~yEeU;qEJ{Pzp|pOucqNnlF=0*DFdB%s0n ze*I+l{{11(um4^iSXF}z{SS2YKY#!N`GNV*AE2Jys=p_Au>JnK{@0e3zk%WO_xG>A zzkdJw^XDIk{pa6bWqzfjEk^+Y2&CaX+n+VzlIN6JC4s^J?+-}zKcEU=eEj+Q{|`_P zFrfeb|NHmfUxq&n00G3<$-qz^$iN`P!0-!({va{_z!?9)Bm)CLfB^vHpj0t%_B3$- O0000op82)aAGO;3n0AgYI#891D4N?IF|Ns4E{L7&Dm*MXp#y@|U|NLS4 z{rmr)KmTFmmra`h0*Hly38?u0llM=#nEx6H|J4!x{U`A6pVzdvv{Rd?JIrI9L zs`Fos+0`Q4a^C}Q0+k5A1KaQ$>Lf<6lK=vUh2aka(D+|}f9<^YM_B9kC$A?0Z|}3+ z`ptao7t>dSY6f5^0o~2O01!YR8-T_G^}PA>yZ7X8cHUpx=KlJ_{+sC?ST#h$AD{+c zSpNn30U&@_7``$5{_{tYU-I{#-$2g*Lz_YNHw#c5FVJd`!65YaF9ZRd1Q0-s48{x$ s?-&?Px# literal 0 HcmV?d00001 diff --git a/static/flags/pw.png b/static/flags/pw.png new file mode 100755 index 0000000000000000000000000000000000000000..6178b254a5dd2d91eeaa2a2adf124b6dba0af27f GIT binary patch literal 550 zcmV+>0@?kEP)~nkZ zGRMDC%7~XK0S0Tx8Wd7-QG59jKr9SQAk|O4{{Q=*0jSzY^rW5C9>)KFc0aM(`^fh9 z|Np=Kfe=u&EDs~YzkdJ$!~!z=-~YcrJO42FE6xsAYGq`2_Mbt_P-yG_e|4Mhx&31J z|LZSE!|%TgKvw|-5ED@OU$BE2n0|Z9O=M(v1mpqrFfl%gR9?uy{Oi{rkOq*RfB%6# z1PCA|pp${Bfd()#{N!P|4fj41<69Aq$E=K>;Tryc4F(7xpbZRv|AD;q^*8I6-->Vz zfB*5n`o{nM2Peo6f5F}W+3*h_fS7<7;^vxc*?8?`g7w0kCk@cHxaHDByoH$n$~K0fYN5BQR!wAqjL2Kmai^lz(OTgcO%Q oK%!9YKcr{|u^AZn7ytqc0H_5zuk@Q*SpWb407*qoM6N<$f;OS^T>t<8 literal 0 HcmV?d00001 diff --git a/static/flags/py.png b/static/flags/py.png new file mode 100755 index 0000000000000000000000000000000000000000..cb8723c06408828ce68a932ff472daabecc64139 GIT binary patch literal 473 zcmV;~0Ve*5P)@|4`Xj5kLT%`al?B=W5I`&pU%&omV`KRD53KJ$2txq_ zg7fbmh|k3I@68*40Al&@kBy(1nSq%Zss!k?M~{APS@-YvZ#JJG201x|0gQ}qez5@r z5EI0wa6>`h%cteq@9Mue|Ns2{KQdro`19u4 z9*F+^0|+3-w;#WAv9bOIX#?r_2Xj8qX@CF21pWab0}C_Lljko10*H}8@M3d^)Z>@G zf!6*0!vH4#gGo5!|DS*V{{HzdC&9L6!fAj26p=v+03Zy*=I{f8|9_}7o838uP_pBd z=z9{Sra^iHOE|+IC{ z-`~G~{{ZRVfB*dX11A6d`CYbrEkFRVK#Yg0UeQ$d`q2ZhA|Uz&)bIy{egR1#X&Imb zfB<4bR{ihazwp{dkXn!mkfPsU#_wMs1hfID1|Wb~;Esc6TU=EJGWFLlunkbN!Q9_} zL0)432p}e)sSwo=4bd$f5a~a^fBpd(3|0LbOac`E1P}{Q!+)sq|NsB}pI=e{a>g%& z)4?|U{s;0mNCQ9sG5v!Z57O~Jp}QY!5N0Be0)lgG%);U`26kn-@hOg zU%!6+4+cOs(0HIde9xZz`}Onxub&LUB0x(30+2WcIRJn#2ut|?gWYu1Cf+!-K%B8# zdf?1WA}#uZ8oj7u>$I1i0Al&`=O0k%-@icgAIJnM0xA6maSq6BK-ECw|NZ*S`0Lj% z1_pot6puj;05Ax`F!=umqj7^frO?t|3^&I1kxUq9yECc+jQpY84SWH_0#pxl$?v~F z@*hy-KN0|X07U)z`4{NpU%#2aHUI<=%a31wK(7Du52Oc(|3O^?R1IN+RRjI-n*kVB z3=9AP#PZ|EACPLGJ%9cJNh|>9B%spYzZw7h1%?tp0I_@ndg9MNE>313@6R75NcceF zkr51-#U+7;F#`Sf7i0rK0I_`g_NQ&Zji7_t0d_@@ozArL?Su`s;*Q{AKpRQ~TD44PX+*q00taD0I@Lq1k(S2nt`tR{g)9*1H=ZfhCe_Je;NM%1^NLXfPkU&2dGq1gcanC zzsyWb(hLmW7#V^70~+_6f#nZ_(jSmgFakOWAb=Pd7`8Gn`~pjZT=;x%FbfOAzdzu3 l1(F{a{)I#17yR-T)##NTdjqb^wzQ(`1@?t)Ix4MUXz556teM9A7Ic zq_@itH|pv>q+zrjZJ^Hx5bj=fD{5McI3ol<@^-l_@~tZGV7p>1CU&qG~{YccyC-q z$8~P)6sG{nMmQy85K$E6L33rja$x-b9$ literal 0 HcmV?d00001 diff --git a/static/flags/ru.png b/static/flags/ru.png new file mode 100755 index 0000000000000000000000000000000000000000..47da4214fd9edb383687c1d4f84fe8b42a51ceb2 GIT binary patch literal 420 zcmV;V0bBlwP)X|NRSO0LlM<{-BURBqYRGSojej zfLOL~|EH_V_~;P>Nc10*D0|Jxss< zFi1)Q<$$6LU}rIc*dU*QNFV}+9T))>0|XG`SD?F)5CbX~O$rDA0t^5@iDe$xIAIn5 O0000N_~0!B1ZtR02zJmAl3i>|Nr}+@!x-t zvcLa8?7#n*fB$3p|L;$z$Rx)9zm`4EX`B2HAb?mPX21FhQ~^@@2dMNf!=L~E|NQ+A zWdHd6KUK1c=hsc9f8Rnyezh^=0R#{e(Ek5GMIci_s{j0D`2GL?@4p}ozrlKzzsUIh zhw0@n(cNzYfer%*AeP_1{{8vS`0Fp&84w#lmi+$#)ARI~X!Y&+KyeWsW`<9n00M~d z^G^m==F0#79{mS71ZX;t`~_q%fY`tPf!Mzw;*3o4FJA8j2p|?NkT1UdXZrG=;TOYy zh@M|qNhT&CJ~jq`0Ac}pl#vl6#K7 zFA)9u_3syi0VMzY2QmHtx&Qtu@&7&0&HxaAAvs6^7=(cU7?W7&6Z-$piRe`dyDK`^ zN$WO$zWL=wEu!PO?Vu9@iVSM&8cWvf2p~p=WCn&G3_$lI&>tl77dYY}Tp)vm0U*Et X__=7oxWDB`00000NkvXXu0mjfV`BRN literal 0 HcmV?d00001 diff --git a/static/flags/sa.png b/static/flags/sa.png new file mode 100755 index 0000000000000000000000000000000000000000..b4641c7e8b0dd79aafaa73babdb525d3d2dc6a8e GIT binary patch literal 551 zcmV+?0@(eDP)4!1u&@QJ10&`rk^HbMk8Ee|uIe&H zS+;4$DbWCt7$DsBz5oJg0IGiW@flDZ69W^F_wDDmUw?kFvatRB^XvPsAO9KtfBpI8 z%a6}8Lb5=`00G1TbjJVx|9}4ZVWnuJEUfzZ$EVZx<$*voWzTGcp~$d(=+V{_?X+ z3y#kPS_RYq5I`UeU<=TsD9Bk~@o?qi&=DPd(?v86a{(`&&3I*E&g8|Wo?Sn;v*0j086Wc~d66UYT>00&^E24DBZ$$%)DV9SRUYEY{Wn z85tspN%!Z^Q&_X+!-^Gee*b1;`1hkUW~HJ2(d(c7|`w2)m##KhU^#|s1eh2YzMVW-Z(;)C{QJl7?-Rp6DTaSP8U8UK5I_L2Ffe?nu66~hU^Xzg zV`^IR|NmbGhChsd{xGln!xZ`Z|DQkqe?!SHn>GOi5KsdU{eSZ0I}_6%bMu4$|G#2j z_zP0e_xu0e-~WI8{{Q>e|6jj=B8;-KKpOx8hy`TyKZd`5nT(9?%F2RO|M~TQ@}K{E zz)C@?e?v4d{QeEp01!YxcLP=bXJGj0<#nFn|DXSVfvSK1-v%}nhwkOu|GvKejg0<-0ptJwK&1cy1oE`H<`Z?5M+~$7{9gq& zn~(3mv&(;P@Bi|05T(EW{(k!9>4UEid{_Ab1OPDr&i@1e0Q5gX)c>UW0I2)_6d3&w~i*%}NQ0G05Jg0{{#gAwk6j%|K36Wcrx-Z zDBuV5-R-~I;@8;l*yr}=_xShx{`~y_{Td7!{r>#`1^@zxvDKIVAJ?B1c2ZXvZZlkB uU;qZ}2Vg*eW0eWa_yywvNgf7(00RJ0?>)A@UfIF`0000`tbb&P~tyW8d(D~GxMiUp8x`gh2bB=pZ|ZjUUI!&{yK1G;4d%$ zIuA;MS%3cg{`2Rrii*m`ix&X`h=t(`1IH^)u0vdsLXv;@{sQG7;18Jf2Mqqg05F6A i0*HaZmVqPy5MTgGElxU<64PS<0000a|fPuet^$h(7pHv_{jfB<3vn*8q{15ov!pR5qo|Ns4BVf^v!57RHO zo?l?}`yWs<(7C?=0*LYNK?X*pGKOD3v;Q#s|MUOfZxH$qCjb2cBQWFtZ$@_cPkRpn z1P}|u9|n-AAQdnT|9?TXAyk7H4FCQBl>h_~&`AvcLF)bhMS(&{8jwh^2qVzve;5D) zh>=0RIIAq+{+o}$pxEl(3%2T)`P!1-fBt~{@Pp~sA7J?W`}60ovKZgl)=2;X#KQ37 z&mWM7{{H^+4`$e}UqIEMAo>Rk|KALM|1kdh!vsX^3=B^I0*D0|DL|!?{M^6*VPKPH z00z{*-?BnHps)wJ`QKllIe-8D1EGJQNCgNWMh3}bctpOPvlbWzK!=K^+cJPc;};D5 g19O2S13-WQ0NlBGh$rR(5C8xG07*qoM6N<$f_QiWUjP6A literal 0 HcmV?d00001 diff --git a/static/flags/sg.png b/static/flags/sg.png new file mode 100755 index 0000000000000000000000000000000000000000..dd34d6121073fffcb2fcb5b9402b3e6361cded35 GIT binary patch literal 468 zcmV;_0W1EAP)dtOYis{Le*Dj#JuDs`KNc)tl9OYUmifJZ zA4uJ=-~WIA`uFP>kj*SB3$z3vfIu4l|NYA>FaP8D^M5;ba1<8<4f|GC2UPm+H&ER# zkRGsxe?Y{*01!YRC;dKnkj2`X)zkCW_8s5b+Wvn23^5y|0jv~A0#!5q{{4%A0U&@_ zfL8pya^=_l{r?#l8F+XY{;)G~as30ko(ZTFY%s&0Ka9WsFiJ`S6$1niy`;Hnv5 zfRT}r0R;fL{~@sn5I`&p|Ni}W_wFAQ`~gBVzyJ~jk&qMs5MThRyiZo6SsHx+0000< KMNUMnLSTY6dB!#X literal 0 HcmV?d00001 diff --git a/static/flags/sh.png b/static/flags/sh.png new file mode 100755 index 0000000000000000000000000000000000000000..4b1d2a29107be96413eb86e64a75ac7a3ba5793d GIT binary patch literal 645 zcmYL{Z%9*77>Cd9?woAXO+#E-F%m^1b0Xy*Qky9{B^@hJqB6}jOKKPc70u1CS|Ug( z7>SA^1`5-}4+VvY=3G*k2%(8O!OWTIn!4?td(P>GANugV4?p-l@2B^fCONIbD;IAODX_{rV|BCn_NC>%qlWoHrzH=l|0Y^Rhgkwr%>N3 z(d)FjlCqjgyY4&yRH!;rb)|Z-v~HjxIkvar`*JLyzxBc-B?Ix`3*qGz4q3JAd`#LY+Xw^k(ph!n`d2H7`aI`Eh(LrOLs%9g zj93;8ws%s88WHkIqXqnSf?YSjh=@dF-}4L7dS0HFB@iNj8OY*&4>%Dn8t&*i)aXz6 zSX_wQ?~e=9UcwhrAtAf8XLVoTbE5+<^|-KK=D&>)yX6u!zrPCrbEr|4Yi(XyIGTQI zFEDsraAY{)DhUd*DN;Q?!uSxvkoT|31dF#>2L0DGeRcNZNehm>xm~}-9q?gtV@Qz` zv-lB19|m}3LHcg92}TUOb+%v(0bnUhB(5rQI9?ZY)h~Hw=%2Au&~WB@t;^kVE@F0Y z%=8f1ZN}R1MniiNxkJ!a;3!XFerfimE2A;1XJChGXJ=)MAVRubE8WFo1T(1Cmhdfa ztzC{Qms6asjkstFkFp5L#maeek84Y+NtW^Wf=SRytjpC1=BCX4NH^VxnQ`+YXocAv zR?lKskkKZN7D>{S3>4;4+gPYYq0_5iq@jsB^}M0yMT0|p`lM;R_dwbVrBg^4RRbsq Y$WB%-43-yHbAJTXS^1gPjGK@C0`m$%7XSbN literal 0 HcmV?d00001 diff --git a/static/flags/si.png b/static/flags/si.png new file mode 100755 index 0000000000000000000000000000000000000000..bb1476ff5fe8e0d3af4fc6bd11e513d95fd9cccd GIT binary patch literal 510 zcmVm9@#;PEzrU@Gx(rNA|FHo^Mn<3|00G3vEO>ZAhtlWIf1*N!=PZ8p<;&lH z{~7-L`S<7dzu$lU0~x=8==VP$LqeQ==EOq)0mSn6{g=m2e$<)X`O9Zpu5&$Np~P9Kn=gZ0kf<1I-4a zUtk1c`~{K>fBu3(0U&^Y8d(1RW7^4V^6LZG89>!QBmXe{`pfY9FT=0Dj8O6)!|#7g zK*az7#L~(TO=a{vExMP2%`MCSoB^FIcLe_%lf;|~%E5I`(IQNh}3Ao>6Q|DFUXMn*>AqQd`w z|1kXd^B;tM|Njjl{{hM0zwd6?1Q0+hV1xeud-4=Wy?p-%sO`^#2S61Jzk!N?s)6X& zzhA%p|N6}=D+{y%Ab`Lc{sL9~1=0UN4*CD*7s%9KAf+JHKs~=eB-8KTKvw|-5R1&; zzd&a|ob(5%^Z$Q=wHy9p13+aOpFRNu5F>N&`Tk_-7w>=n{RejQzkfh&Kn{rf10?_b z{tFTZibx5v&dxav5I~H7|Ney-|DWN1$%1FyagzUW0464;_wU~W1Q5$TW@eGxtUvee z3vAf*8|igK9~@*rr66bh|NrkNM8z+V zAV?>O@ek;bKfu6d002F*|Nr~{=Pyu%qcHbB24G}l{PX7zKmY(S0M7peLS*D^UKK~y z+6(XH|9gD^r>6bY*OZ>+`2GX^{Qmv@{`~#_{QUm>{r(dW1b1xK0st`p&i@3&%J4-( z6Giy=|7vRh4-WrbUHx`??d0*<@CX3>{QLa=`~Cm?`~Ld;{u~zu0R89!i0SVi2B1U! z{r&fU#+3gbKmA|6l!@v8=U+^J{{8>`mjUR&KfnL~{sUtC1qTg400I5L26X!4i0my} zo;{uZo#CfPX{7DT1DwDAf(-fnkMZ|^ra%9I0mS$d6bftr0mQhUfkCOfiE zM%HqlKU{maemr>kAILSo!3gZ8-+$N@WcKe}3J^dnRtyZx@9+FOdynDWy$P0%j62V; zZC)?>`}hA}zy1SB2;@n0006p>>h67sJD!pI8@uK6+#GPW%ce@CEFZM znTD)%K!F4J_qpc@AQp(ZPk%rA2T{TB=kLG2fBygZ4O9eD_xsnsUl6u5yEM=SfB<5# z6|+@gR}o|u1R4pn+XF1YrQf2oylS zAbS3(sBkhcTmc9mV6grHN=r(zLW3F{=D&YK84x>x_WT70A}|U80*H};VI?9O(a}F7 e1_Utx1Q-B;QgQb4eH!Wj0000PEol7!5I{@}e;EG$`~Mq){{FvoV+so& z6Zek;JYV*``O13q$!wq?2!V|I&+z9TKmf4-@!$W9Km&lr1Ie!(IeUNcyZo#F`{&0p z?&{CqnEw9%50U)~((nf$fIu4l{AK07*qoM6N<$g2hARp#T5? literal 0 HcmV?d00001 diff --git a/static/flags/sn.png b/static/flags/sn.png new file mode 100755 index 0000000000000000000000000000000000000000..eabb71db4e8275a5bfb7b1b8f3a8374d50da95db GIT binary patch literal 532 zcmV+v0_**WP)6{xSen|M~Ox&!2yPfTn)={d@hr-#`EU4p90fz>J~+=oo+i0%`d3@9*!wzk#ZO z2WQUEDXOGe*gO;$t?-?1~^z5 zels!xNuY7SApXOk^arFAjDSu82p~p=WCoxfknArIdOZOgs+`19}IpFa%0{xSUi1L7diFBr+f01$veIX40Tgn<}np4MoMn3N`o$god9 zrkzn;+j{#q5F}xeh49xZuI$05IKi0v3Lq917Le*!UxBJYN`Vr8|1$jj50VD5fvSOw z-(ZF~46zx($8#~-kUKOh9u19TNY0I~dG z`1hBA5y<}g_y4cI3}({*g*gAq^Z#dL_#dnH|JmpNJMaAe2etv|3x;0|00G4G>kr86 zzYquf`+xk=e@^EAQat}9xc>97{@;He<|?3qzo3u+2q2a}APrzwf*tnl_y6c6m z5?7266~G|=0}2mdsDK;{bP_-SF*1~XW%vY(NDyK`u#uw~6h{mUJPZH<1^~BhckWKq{Jk|L6Yw|Nrm*vcLXk{sL#*k)f0!)jRF?KX6FHLLC_7zyJOQ#RJf}AnqSv4E_E0 z_d>^cfB<4-C}v>z1dGUDNRj#r9OZBkNc8hC00bBS^Nnc?6(4BA00000NkvXXu0mjf DM=x{`~^7feaw| z@9%GBrr%pd7ytr@3Fu&;D>$G0;d%9&ckUlP_FsJLznOpkI<);49~%fU|NOOg<1c>p zU%YI;<@gu?0tjRS1JJ?0e*b^_hvDg;|Nnpg`SWY#%HJzjff!!CzZWj}wP?w&U%!5b zMg95nhXEjfKpOu4{R2W=zZm2}8km@V1qJfIu3a z^ZvQT`ny}__wU`%Fad@Sg!~P3A(#OSI%WR9r+OFw0*K`e1H&7RKZ`vj#ee<&!}=E_ z2M*0Ye}N7Llfc;c2aF@2)L(`#3;+SdxR!w-xR`AxVx?>``QKoX4p z{r>$=Qj`^FFF*h>?mzuiS(feBuYdplGyMPm{~rVab^il$89+1;fd$yvm=2wM1rR`t zfB*hvWMl+7=|4yZ16bGpcem@l{$rO?srZisfXY67`UDU_ED+y9gdmKw*T0{<`x@v9 zlkZkl}Tegi}1FC>)y z`~`;fe+5al?K4jS1P}|@5C%z67NEr6KmWj?{{J^f1u*QH|Ne!BH7L;kfnp0FfEXE= rk23safJY>Z`~zeELt=n*00bBSq*!cC{}>3t00000NkvXXu0mjfg_GrH literal 0 HcmV?d00001 diff --git a/static/flags/sy.png b/static/flags/sy.png new file mode 100755 index 0000000000000000000000000000000000000000..f5ce30dcb79b443ebc1615fe4889cc26e2d762b1 GIT binary patch literal 422 zcmV;X0a^ZuP)@|4`Xj5kLT%`al?B=W5I`)SK7Qoo3V-qKd5l^# zgv$taFhBq?zJ2?apPwJU>mjm?jEr~g+yMw6Mj$IKE$!aDd%wZp7Z^YoAQH^_^XK=U zKYvwJR4!h;2oOM^VEz63kEEm|Pzoptl!JgjVAdZn_zMF-#Q*_BG6D!N0MDLEDh$KY Qwg3PC07*qoM6N<$f z|Nj36M1Mf!|33`>e*gaqWCO|HKwFtO#a&sZ0t5gt0M7pel4t!zdFAutyc%F2ByLSib7d z-@iY8{9s{WsjaVha_-{UlG^LGvEO)A^**!$6$1ni3ljqaFhGRYPy6$qf&JG{kKey{ z|N8ae_ixs}{}``bdDhkT?#VOp#VvoBm>H#|fo1~)5aY*A1~#Xrf1jTKgW~`H-wePI z{Ri~aZw6pM{r$!84-_k)u=@Ltg1)Cf2{xiF#iKZ95W~`fDE7~ z|1pDN1{?_>vH!pR*Zxoe2p~qo;+=0k{eVOy5dHe~@Bg1a48OqWACLsaJOfY!7)!uZ dz{3I%U;rfUVNTmRI(Yy9002ovPDHLkV1m_xKPvzL literal 0 HcmV?d00001 diff --git a/static/flags/tc.png b/static/flags/tc.png new file mode 100755 index 0000000000000000000000000000000000000000..8fc1156bec3389e54d3c5bb8339901773a881e68 GIT binary patch literal 624 zcmV-$0+0QPP)Mt z2Fbqp@bUfi_XiXZ_x%As_WHU2H1hfc9TM^w3-JC0{^0Bg`~MgK0Qv%mWs{6kqN+Cc z+b3Uc-enXLWmi^s_2~6APR{Qy8GilxQ)j#p({t5y7`^LQ4&lw9Z0Ra5^`SSP!?%<&3?fU!$2L%N9{QCa@0Qv%mW$D2m zA^~iFet$Uihac>@UpF6ozx?O#ufL4HfBpRUndtzp5U;>@#TUPR{|1KG4}bt-0)`aO z`@jDF|MM3p^6S^{zdwKf{q+mXVfa4fgq7?27ZxvH%|34Sm*EEk!#{ukVgYLS^A{)$ z3^fq>3urc&4I=;kz4MwgW4X`2zkh%J`Om=vauPrQ0X6*k{pY`=2p7%A_P)?P zUwuVdAkKe=-@h3}_C6p$AOxEwQo+UIeHT5%mg3lYGL;@HP(LjqG0$?6F}(Ht8A z0K*^*BsuTDFa|;zm9Mc;PRcq|KMMBO%8|{GkrU*a2x&r-3HS3`xnqW1|R~39RolBu`vAj19S<{HjoWK z4L}tj8-Qm20eTwfuYbROLyY(bwgDi3SQvf*HG+%>>H*pSauQI_zkmNAP67ugNCPlf z{{97O00g6G8FFbZ~XHln?_bqJeH^V)*lb;YTJ&0)m(r7ytqc06HRil3NF`RsaA107*qo IM6N<$g5!7R+W-In literal 0 HcmV?d00001 diff --git a/static/flags/tf.png b/static/flags/tf.png new file mode 100755 index 0000000000000000000000000000000000000000..80529a4361941e01d1def5d581bf2847cf99fef6 GIT binary patch literal 527 zcmV+q0`UEbP)KfiwinSYl|od6I(APxT+{=azr6>87_{|pZw{QvzM zh#vfW|M&NwU%!FG-`~IfN=xzq?EwhD!Wi2C0Dv&)|Bp?Zs+hPi0R-LQn75%cY-O8s zAPsw;Q!9X27=Hcx_y6DDzyCn0!4P6RP{Xg^fBpb%_yyDhbQs7f8-Qku33L4cS@IvK z8OZ(n2kOgTzyGb@ek0IRW9ouaN6$P0x*s5bfH4)06qlIj4>;1{Yyp4(0|4dQTo>gF RMrQy3002ovPDHLkV1fdR=hFZH literal 0 HcmV?d00001 diff --git a/static/flags/tg.png b/static/flags/tg.png new file mode 100755 index 0000000000000000000000000000000000000000..3aa00ad4dface0a9c23744ab451cec0443f187bf GIT binary patch literal 562 zcmV-20?qx2P)@|6mN}b_RwI3=Dr@=ogG+VE_mq7La^(HIVqv%*^=nCj$%1|9}7Uoj(8m`RC6+ zAo~61@1NiQe*Xrt|Nh=Ea}z)Sfi-Y&{FjhmFg0aZxaj}$=L~=U)(L)O`TP6-zu*7= z1Ib@N^zYwqCU)6YF9v`BVgi}{>lcH)J%gU!|J)p)>i@rg^Kt&>WCWsLoQyxgk29dnvBpN5vQb0*LYVQ3gi&tp7jnGyMMp z@*mKbe}4Uk_!#8f-w;8De}5R+mEP<>2M|CkH{LKvi2wQh{WmZi827ytqc0C8?ZF&p#S!~g&Q07*qoM6N<$g49|K A6951J literal 0 HcmV?d00001 diff --git a/static/flags/th.png b/static/flags/th.png new file mode 100755 index 0000000000000000000000000000000000000000..dd8ba91719ba641502bc7ffda16c25dc71b2066c GIT binary patch literal 452 zcmV;#0XzPQP)@|4`Xj5kLTn#2^O%5QJf({+!S7KyMr1NSdb0?wsyYS6cNVdko7wub89?$EL;)x)00Ic8;m6; z*ldXL$Yz5{poPB}7`^}m5DPc2nu@9r4=+1782$pC1a=$HjDKJ@i17#P6G=(#Q>V-T z0*LYF&!5c9%>VxV!xcCX0F(uW(%ZLh0Ro8SA3Hl!NeKhQjeq|#{05SL7=Ql;MhF4{ ujRQ&nZP~jQAb=RZqDCa1IEDa#00RK>mSUL#9?hx%0000(5Af|u+{y|g&!S9Ft%(8imf-XR*|Ns8{XO&`< ztN44b9;zJZ^?&~W0*LYB$B#TbJpUo){|D*(`>^x>w|gKB?9z<#wQ%E^n3(S0zYh>V zEO6uD3`W^f29Wmu3`mZF`w$?27#UO<(rVJ~y}I}N&+p%VfB*XP3y6U1Uw^?QNbuL6 zzrX+d{i`Cbal7Zfib@b53s27mx! mU=U|u;Adc9XCM*)1Q-C-v^a{iiydVE0000+lNu{Qmp=|FW6D0*D1<1Ovm9m%kYQ|7p*Yo3ikU z7~kiESLFZv{{Q#iA0zqwf4<*Xu-*CD8$MYncA&8U0mQ`ckKy0n|9}7f|MB&2MbDkQ z1j{KKBL!IA2(Z7F;(zkuBe#mo9TT;SzkoLU{s(jwKmai@{QdX$=kGt?etVi7(p9*e z(|o73K&5wiaQBkXEB9so{`tH8jIplb^d$ZPi5h|uLD4*o5RnOS|{Qd{@Cr~j!0D)}y0}8T#$FHit{lE#dc*g~k_utsL zxj&Wpto`+k?f2h5mv;mM4gK>M6!-uE1hnA~P_?8O+wWh(`!4wX{Cn2j=z=is`~Uxb zzx=@K_DcEUJ7IZAUZAu8{sn~+Kmai^Fsw$4O9qB73=GeJhA}V*Ffg!yW&gnt6F`6g Y0QOrYft$MNbN~PV07*qoM6N<$f;FEW z@b~ZkKS1)&|37~i{`~p>`}cn&^2?@800G1T)Nta$zvt>t<757@uK4@^{4bCmpb7~3 z1tfp{|Md&VW|Wov$G`v(Kp+kO{{26+>+jbuf7A2+upj>Wf7$Q%RxoV1$N0e}Fas1Q1BW)@@rKJa|xATBWA`D>(WW1JFHv zzy7cP0k+{MTm!^O009K@!>LoJmn~avX#S_9;5YZ-KmTX``u`YaHjD&?*dJiPF#rS* zNW;&cKP?=87yABYZ~OCq-%n(9P{tpi22h9t6$1niNQ1e-pD1xDrvBdy@BT2zfI@>2 zY$OAO1fsuxAqeOsfB<52Wnfst00RHO2+qJn5C9Nh04p0wT74^6IRF3v07*qoM6N<$ Ef~oTCkN^Mx literal 0 HcmV?d00001 diff --git a/static/flags/tm.png b/static/flags/tm.png new file mode 100755 index 0000000000000000000000000000000000000000..828020ecd0f6fc73348373c9e7a235fdced09de7 GIT binary patch literal 593 zcmV-X0VKiqu&g@GZ_kb&V31Gky(*8`UselaloV1SZ886W^4001!n&i@1e004Y* zk0R&!|NaLC5DIk$@CyeC@AU9ebDYiJ)zswL{Qdm<{`>p<{rmm%TMiuO{1voVt6d+$)WVk%^O){nwx0zkdJv^%rQ#Z=eQ%0Ac}Z`19}Y zkI%oqe*ekA`2WvehFzDBOo*xZ{rB(cQ`_&qdGhP`Z=jyvAOx}jAb?nae){+CFDvWc zZ(siLa{c@D=g;LQw^HX+sY@u`d-M3!$G1SGAWQ!Ifj9{ufLMTG0<_`#=U?|Ye)D|< z8pH7G?=PVA*@stt1I-4xpA0e#40jk9 f${7d%fB*vk3(!F(w2U8u00000NkvXXu0mjf+94t7 literal 0 HcmV?d00001 diff --git a/static/flags/tn.png b/static/flags/tn.png new file mode 100755 index 0000000000000000000000000000000000000000..183cdd3dc98c6957bde83f375a431e543a3ce9e4 GIT binary patch literal 495 zcmV@|4`Xj5kLT%gT|L_r@28Q3iff@h;hzaCwplYy<4FCT9KXmB7 zhQ|Nt)Bg(#gN*m^{Qvt8$Of<_K(zn?!~}BDpT8hi0uj&>pxItt|NHv>A2{&e(C|Mm zFGw7s;m;qS=>P!)((wQ9AF!W(0sZ}7M&|#9_5TYB{zpfHZ20g2t^wjCfB*tH2_nSs z8?4R7=Kq6S{f9wP5^T(0hI#XVIvK%6GC)WrB}fdh6SO{nUiXv3iAlW}aH!uJM5J&?vILH{_iZHeP0c!XQ3PuJ7 zfB*t%VEX-sK~fTI%wL8-Af1e0BN-qh5dHlNK|sX-0mS$fH6j^sMDRaYQUC}r0RA0L U-~OCsp8x;=07*qoM6N<$f)6&Rz5oCK literal 0 HcmV?d00001 diff --git a/static/flags/tr.png b/static/flags/tr.png new file mode 100755 index 0000000000000000000000000000000000000000..be32f77e9910c0896c1ee8e7ed4f0edf815a517e GIT binary patch literal 492 zcmV@|4`Xj5kLT%yy% zCxE~j7#aURc>+`lQtjr(@bBOM9XtMi`UFz<>-YcPzyAOF1!ObI$^vZw2q2J#|6qeb zs-vSBOie-Rl$HN?bb!_U0_g#3VEFwTr~x2=m_Y6Zss`GqsL0^t^nd#F|4B*zH*EO- z_3QuT%m4H6g6)A?0#pkSKujPf{rCwIb#eU5`( z{s2t}2q2J#|9}60{Pg58FvR|AYlEDjruP5RC6KEgJ^H_IA4CJhNdN)F0>q49OBog~ z{?Etvzo-c07$As@{Qu?4|3!FoS~(Xu$ujuK&Ki|5;f$7pYF;Nfxh(xqFs zZUMDjzjMbfEbRY>5C0Dx`v3aP|JSeozj_5kj66I)?%V+g05Jg0{{w&k0RJ5w0P^!X zSy|WJ-R9=z(aMV97z{4myy=pj4$^bx3|zy{hYtY)h`Ao9!QA5iyLbPuTmkB_PfP?Vee~$j z!-o&gUAyj)n+wu&>J&%=KmU)%j{yP*q=CWA45Z=rFNTYkfO?!$Q_o+&{s0WlT)E;? zRQUhj|NkdWF!J+%fBF<4fS7B68jOwqzkLe|6QI@S&;S4b-#s()-1Y1C!Qjl5tA6F> zpisYc>&x@!00G2Y!@$5~Xb92(47=Z8{{bD%z~Ggi4>bPny?b}>+&OjWQc!g@0~6Et z-Maw-h`Ac5K~JB7o9jOp7XvpB0}lu>-nqlT$mpA&|LpDCZ{NOs`t<42?c44-IbZwx z0Rjj;B7x{N1H(%OhUW|nPgz+XA3Vr_1a9x%4G>@ep<^{rq?UTJ00000NkvXXu0mjf Doa;3l literal 0 HcmV?d00001 diff --git a/static/flags/tv.png b/static/flags/tv.png new file mode 100755 index 0000000000000000000000000000000000000000..28274c5fb40e5d3bacd7c05d9a1b8017eeaffa6c GIT binary patch literal 536 zcmV+z0_XjSP)mzpv3Mmw05Sgm{~xFtLc$sUu>nR##!sI<0R#{WMEdpj-yJ7j ze)|0V&%b{_!~X#7`1>EofS_N0|E)GoUH&SsFy}2m05O4F@}ffYb$qvjbfSUipMQUU zgY^9U_4m&&5D8KZ)HC5u^uad<48Q&W1P~L@hOIAb7eDp_T4W)^*Xg48{r4ZBJ-_}k z{QCPJq~Q-pPnz693xQJ%zkUG(5aZwHtqd&u|9`*u&+TyOvq{_Ghd=&6oB?#w{~Yv(xYPgr|NRH*haKioowt*& z{dC`3v>70P7=Ql!$;`|Q@(GT>fdHT^FqGcDeG3pkjNS|k>E&sEfzAeo{ckY&=g(gd z`CCrwmxA8!XLtTy-uM6CzrVnM%iVPzAb=R%ks=j>et^j`MFs;Wh8c4i)_^1XKP)u> a1Q-A&>r+tW$-Nl>0000<~s!0-E7R}a|Ns2?4|9|}gs%Dgx1=;`*KrBFO{{8#+``2Fv2B6@-@8AFY{`(K80jdYk13&NH4;SYnOGgJvc0I@K9VW@Vk1}Xmk4~YK$|NH0P z-@pI<0D+Rop8&DnA}oIvEc_iD{O9-Y-=|4+U?1uFgf@9*z_zyJRJ z%fS3QR^zuR^Y0tyerNXndi(YlP_?gzto2+5fB<3vIsj-gQ0X6_*?&Q*d^>boB3^zoi8Kq^tbl{qlR!{9jwQ|Nij< z=(=A?F~8j-MSe1{Hl8Yc@zaXo%Ljk}VgUvb(2HJbe?0kqy?_3vsPXrUmq2rWE6M!I zE@4*I;Mw}vw&7&iFGdC>zW)rr{s9CK3xfnhrsi*1wqMhx{@J=6sPgxpU%z6)f4fBr zzxpd#a3tyYE8}0lKnA+|-+zWbe*glA<)R0}-=}{Pnt#7~1q_Mb;=;f3i~oshNgsS> zIr~z~(_bRL|NZ|1LO_>;A_pLVSoBpHQd9nLa7ao@{lMMx#__lBLe9Cjx?EiU z71{s)`Oon8FXP|;z>s7BItd_v7-5mj(Adb}701wVj$!S*>kNN{fN~5FV)t0eYh-DxnQ(+0I@JIfmA>F`X8wD?>`2h#NWRRzyE`zA&Pzh5tt#% z!^rUCA3y+sZ1@LM1hf`pHc0ia|NsB|1uKPX0CRu;1-SztfLMSo`wOxir1}rsY$VlC z667idh7Sw?0mQ-}a!Q0#;n&}vAb0=!_Zy@WNd5!6=O2ju7s{65Vq=gx0uVrqe;+b1 z$mIY3|KLBvF9x7j{{!v#g_UGtQhI;<6hHv607H@yECdWlR7EJN!LomVp$!l~APvkQ z4p0cL2#YobhQA>6hXEjfKpL3-19eKmL_s!=;22W8vpXP)6RGKQPvx1q)UK1Q6rb{|u}Q zRsa7#{m<~1f#L7}|GzNK{&HxZVEKJ4>jAefr9{u_M=MTegAo-i2ynN}MJ5kZm z42KRel$J95{>|{~*Z<$Y8RdVo9Ap3pAeIsa2H!t_KmPs=bot-kKnMR0GBy2s>sC=w zQFmwO;@`jj{`v(%zkl=p`BTQg01!Yd{Xp;j{qx|@pI^U!0X_En_iwRd$6{h)tX8f( zaOhC?&!0e%U%!3phy1#1%V=eK#YHXK~n$!{#(ib5I~Gyfqpu4^P)1J@ZrOUii!$=0Ad7ci1(2A^!3kwFaT->5i7Ttz54h6XL$JkA3s3o z=g)t?e>01U3h&zo5I`(I-#Hr#t~u}$WXZu-Kyv1Rm&rE5wcB1YFI@@=0EYkn8G$4- z^Xb{M0Ro8S57@^*QJ}#<$Im$M^4iKw z00a=@`Sa(sw6y;I{R{T)e~6KA2AmBHF`#D(ii!XNh(#nN}rF)(}plRy9vU;su%e@J`J?dSji002ov JPDHLkV1n4Y8}|SJ literal 0 HcmV?d00001 diff --git a/static/flags/us.png b/static/flags/us.png new file mode 100755 index 0000000000000000000000000000000000000000..10f451fe85c41c6c9a06d543a57114ae2f87ecc1 GIT binary patch literal 609 zcmV-n0-pVeP){qW|y?pud`Sa)3|NY&vWd%S0u>b>P!2!lUe;6EF*#G_c zFVXVt@6Q{uX@40W{p0iY2Aa+A^Cu7i8KT+YH}2j52q4BskM2rJ$^k9;2Xxc_|Np=M z&VaLlA*IO5FlECMfB<5VUNC{tBZO(|zW*;@GJN;|bTJ71`0*d;`d`2P!x=ymOA`2> z+y@9C##^^8%gd{MW@Y91_2d742B2~OQNf=-zkmD?Vqkdk_wPTUNeuu2#KPTG{_;O4 v7C%8E5*DLB7#Kb?Fnj}}-(W6879hX?8lYRg`Y`<~00000NkvXXu0mjfD6Jtx literal 0 HcmV?d00001 diff --git a/static/flags/uy.png b/static/flags/uy.png new file mode 100755 index 0000000000000000000000000000000000000000..31d948a067fe02d067a8c2e69f28cca446bc7c57 GIT binary patch literal 532 zcmV+v0_**WP)_vmzq~N}&z08z z0*LYY{pZr+B0$d}2MCnI@DC;m3N;oM#uMkR0R#{ugY)L9Y<*xj0QCR^`!^)W!R$Za z5CobHbl5+T3;%B|S`QFFjQ1Zt|MTw;G#Vi+hCg5i(ELAtfD|ak8UBG;ObiSF0R{lf Wla#5zB1?M!0000JMe1P}`Y1HZufM;3|NZsr4^XwNEI-g5fB<4iWtjhPj`qjL zFGT_P{{Q>;-(R370Ro7T!5$Ws$Po%5A+Zb!3j_cNFaSC{Z(fWD@s$7o002ovPDHLk FV1jsy^u+)G literal 0 HcmV?d00001 diff --git a/static/flags/va.png b/static/flags/va.png new file mode 100755 index 0000000000000000000000000000000000000000..b31eaf225d6fd770e0557c2baf8747c91ce88983 GIT binary patch literal 553 zcmV+^0@nSBP)|05Jg_ z#sD@3Z1&&(|Ni{{_Zx)%v%Gr!NnLzkb;C z@#QWhJ{xI9$+*)bNX$S?E90e{K^EMkYok78VXhR-hjM0*K`g z$j^Tmm_>oEVgxdn{xJOg$-v0L$jHRN$OsH@P9Y&+^ne@=1^@xXcy~X;zaPM$WdOPj ri2i^{AeYDB@IM9-1r;P))4B5XW#s0WPsWLbObPT z00IbN14#96puqp%TKvBf)PA*{`~CO-uM~~n%6z|m{re4Z55!3T0R+GV5`UO(`=kK4te?c|?1P~}#f$jmC10=!0@{8gBFK@Zu z-~WJALqveC|MTzfA7GIDVE_mqkOrW8BqZ2?K4D^#WMJTBVE7%b47BbKBg1c?EHl#| zW&XdQF#ikE01!Zo3=CVrargu%_x5NZPzVyc@Ms2-@*Gr#1lu5DU{kh6%sopXB}uDxAo`@Bt_a*7F+) z{Q{%kjEu5C8vp``Wse8Lf5q7h438n|z~nEOsZga5l7ZnjPy;{!05Jg0{{a91!FF~d z;^O)I`~dv>{`>s@o}L{E2?GEB|Mm6!N=n`P`~Ld+{`>p?A0G$7!M*~B3FM_eAU;q6 zNCH*w+4I@c^Ny?Q?QPpWZQiU1R1HBuWkA0H1P}`l|NYDG=gP=Ihyj(0CItil0R{kn^jdV2 S*Eqib00003_-&c2+@O*Bba;fM%rBo$$qwJnuekcf#0k*RG7MM`2+5Y7dK4k?R% z*@ue~6f;u_A~T0evP6qS=VxXnGS_mt>CSmS&gmG`kLSL)e_Vf_=c&!fKB7`4C;*C(7s2O*P zOs1$U0urzb3<=a`d9ABG4Q$eK<6~5Cg~V%B8`UqrfRSN8f&?QTVICs^VT=&p*?Eut zt4Ur&-BL_ON(Z|Xfgo93lf|gRkT$Mz-7^OQoD^XMF@}g>R?uoU0094KbXv)l?aF0E zh@GXQVr04m@05R(Rjxq*p|CCIRfv;QJmH1kf!hsqX*sha?_l;f%e5i5I~4YG#H4;6sPG2&U~bv( zNr`eMXGIX9?wP^zPQP(6+&$05Jg0{{;N}{t6CR|M}we`P%>f+X)5&j)Fc04NUj< zI`HZ2Woe@k6UF22`V|Qe2MTi(6bb?WF#yj00*Hv<{QLmnFd+P z9@fS%;@9^E|H1b5`Vk8^2@Yw;#}(}C_yUN9iGkt%y>Cxm>T2lO13mifZBc!n`jhHL z!MK>$6Z_d%|GfHMapIVnqod%T-`{|q1_&S~IR%b4Z+;63vi@iI?>jBz=eJjkjQ{>J z{QUQa@gMhBW_GSe5B=V~`@+h~@c%z3lmG&V@$K8U{QUg?|NjRX|KX9x@@rf{MxIY^ z?fG>3gQ&3ruaE-Yw>fW_PfAH~0~Or8dmA8tSRg?T(ZDLQOiOw%6RWM9%$i>kkAMHu z-oH0qLBYb*loP0ck&zJ?LI42-(g0BnBsn<*tTlsx%zxbLe*S!R<%*E3>|Z7(9Y#iG zpdch>0Ro8e`Sa&!arp;m4%k`<1H=Xjf<=G;Aix0Vb{`Xo7A9K&0000@|4`Xj5kLT%B_22*h{r-RE_y1qN|Ns8=|JN^|IHRmA&<212VgZ}|A4N4# z+WpUe-rxU^{Q|504bi~x`!`SnKmf4-F(Xhl(8fQ1fG+vp^85d{-~aW0|NsB{f9tRR zg1`PJA~XO^2M8drlm7gLxRT-bpa1p0|1bLmQVm3Azy5pw{{IQ;B%q-{(*XjA3FM@| zf53hMJK)W)|CK-uK=ku}!>|93eu7kk5yVLV0R(o^Z;+)RSAk6exg_KF|98LtFaPy_ z#c#OrKtum9FaQJ)NCPuC$bbfbRI>n`@$3IHusZAC|2cmD|MKhqAD{+Mr~sV<5I`Ue zOuzpyNJ@f@0s0=KlTj9AB*>SXe;D}wK*A7+fQkVEi190GMB<5K2mlB$03I1qT8uIj Q5C8xG07*qoM6N<$f}XY6qW}N^ literal 0 HcmV?d00001 diff --git a/static/flags/vu.png b/static/flags/vu.png new file mode 100755 index 0000000000000000000000000000000000000000..b3397bc63d718b344e604266259134e653925c9d GIT binary patch literal 604 zcmV-i0;BzjP)7(YpZ_rO%ce~L0mQ<@*ghnz= zZvXM=_iyVze?clB=ogUu_5ar|Ae&KE_8$WSKmaiTHT?Vg|MaE5OE>+VAUAUr;?j4FCZIvLVX%uQ>mocOU*NTK^}%njy93_oP3+fBpUa=kKpye|`ZO zzyE?r`Mc+5Xcyo_v6>E-#|Nl{r(LkAS94bRQz)6 z1Oq?-fi&zp{`>yDUnV+0)eJF{zWfaN_0t9DjNiZhfxY|}ME?2z=O_CghC>Vh0R+`!~0W%8&aG{{H+4 zRPp!M??1nPG5q=k)Wabw3seITKp+kO{xLwc{r>&$?_UrCGJgGL;^k4Qujka#`tk7L zjnovNAkb9+0R(gvNCCu>f4~1QF);xde}DY=^Xr#I=|`5hhp|NZyp>({@(egUPqbabS$v+tIbNu;L>xVSv%=>ggR z5I`*V_U`A@(fj`3!N1?XfHr^}17rYw{OcFcC69Z0BvMmfELgy8V88%&5l9HrLN(5b9xs8l&q^Gm-@&XkD1Q1BW|346ee?wJ+H2_`p`^l3V z+1Wtu|3AMO{`_T8Q2{Cj2q2bUARGR$NlJo(=nv2bFrDDw2U!bb{QV0-KNuJQ0*G-b s1H(6@xcrC2{sj~H2V-zBFaQJ?0G2^Lae{Q+uK)l507*qoM6N<$f;6K8u>b%7 literal 0 HcmV?d00001 diff --git a/static/flags/ws.png b/static/flags/ws.png new file mode 100755 index 0000000000000000000000000000000000000000..c16950802ea95b40a4e024be6cce870b1991f40e GIT binary patch literal 476 zcmV<20VDp2P)>E69JA<-5ee*C|6>(%ey48MQ<`3*E1sOaY}WDP*a00a=wNxxYB{9yb2 zOU?HFZD2h;3gJl2w`NO~f5I`Ue%-|qnfGfh(_6MlpFT>xzKtBKk z5J&^l?>`KZl3-*0GW-GQWCR<@03m_s?_UT4Dh3E3#;>Rm$$$~H$WlN65MTgr(_Ikn S3@&c~0000=xie?ZoIZW}({TJKY#xC z@#FjV?*|SXNJ&ZAzkmPUy?b}=+__`Nj&0kvZQi_j_wL4%P)h=Z!r1m*T3JtfNUmdS)dI70mLHE z+xy?%p5eHez0mS(A>sL-rPOy{yGk_s3 zz{tq>=+Ps90Al&~?;k`pNCN|I|RMxbJV00L=%3E(v!XamT}{{RArWx|9$7Z{pWi2U{IKN}lRCnF;x&=P&!(V3Rv&T;|GP6K500a;d#1A^U zy8js%fB*jT=jRXRU*A|5K#2L*r(ZvQ{P_O!=da%umKG4l0|XG%NkAKznV4BvSbj4y zawHV~fBoj~uiyXJSQwZYIoR0QIXM3O{tc7`Dgy{0# literal 0 HcmV?d00001 diff --git a/static/flags/za.png b/static/flags/za.png new file mode 100755 index 0000000000000000000000000000000000000000..57c58e2119f402072640ca758657798b621f3fb1 GIT binary patch literal 642 zcmV-|0)737P)Cfuwe;F7c=r;qyPX>nX3=Cfx7(O#Fd}LsF&%p4OfdL?Z7#j~W z{9>KL@b8bfkaUokt?1q(EJu$q{Qdp^&mV?AfBydlG2rBvO`8A$h{fmzgIW0J+B07s zK74uV#pQB`MAdI!SdN|k|KJ`--LK#OfB*Xb>lcvCC@cGqfdL=@05Jg0{{a910Lj6A zAt&mC*9zzO1pWQ@b1`Tn1`iV6=KuBe|Nj2}{{8>`{{Q~|0SOB6tE&PCq@k>=Oh@B| zgDu0v2b@p;u)h2C^Y4EK4rY$O|Ni{`3qc?^$?%A8m^2X}fLMSU{`~p7aqHLD@0rW| zzIWc|c>Ry-$DePXKfn0(_xG>AzksTL{re5n#{Tyw!wUw000L?F`}dEV6O*(3zu6Di z9{gne@#pKG|NpqSh1vf81DXBz&mW+Me}8zmL>PD&00Ic4!QY)xLzl7RCikV!EWdtz zoorLj&BXoYF88-DfB*gk`{Eza7yo#;S!C4G00M|*elvr*8B6X-zBgb0FtGkEHc8mM zoOScs_b*;Q0~!ksq<=swfJjA!^Ww!_00G4E&x@hvA~zQkvxU59n3VsHd7Nj?ec|R~ zkmuqAx#JHo0{#K*`TG}00$uYDAb=PdGJ(Ek5Vx?d6PS=4{E7kSFNS|$^b5xL14ayB ck_{lh0N_F{UmK66LjV8(07*qoM6N<$f>aVd=Kufz literal 0 HcmV?d00001 diff --git a/static/flags/zm.png b/static/flags/zm.png new file mode 100755 index 0000000000000000000000000000000000000000..c25b07beef894408ae11c3be294d6e0eeb28c0bb GIT binary patch literal 500 zcmVLz(k0+Rp#|Nr~{@4x^5fB>iy$Oe)U zA3pkg`SAPSuU~)vaR2(F*|!oPfLI`Azxw(Lr25~#KmY#x{rl(7|GyyW-|wp|{||Bf z=)Cuc_0O*fYWnY`7ytr@1!(pEfB(To0uBE22c#NA{{8#+_rKq)zyAFG`zKdY6euFW z2yzlY0I`6SGJqWbGV%`;{r&ytA5hcp-u`P`AAdOg`t|1D?}h2=^+y>10tjRS&?yMj zfBygt2HEouXg1LCzyJLD1w#LR1MLR}1V8|>0KE@nffPa916Bvu_V4#Epof105yNkw zzknJ50tl=DS->P)FK#X6%e&u3i|I74$@9uP0*YE!se={)r{r~?r68Z&3 zzZn^2KYiK`5I`&p3=IF7JHD>uaSofgtUAf*-w!6nU;qF7`S<(x|35&+uYZ4j|Njjl zS;T)|pU40ZKrEjoRWbQ1@P?oGFzcPCjLiCzcU8ZC{rmejNW<@6e}U*1kPT$~=Kk_~ z83O}A05QHjx|ikdd7h}_|NsB_wl9j+&d<}^`}fbEzkmP!1q4tEL@O#P9zA*#Ab?m{ zv`qe9Isj6Ah@Z*IdsTw}WYHgB8-D!*lmGvK*?)d>i2vThzyJ_HEdLn(@uk-N0|t-& z`$Yv&?#y3UfPVP*`ya#a|BS!>G5-DsFR)1c1&eHCHYDzO7ytqc0P0;>l>h9)WB>pF M07*qoM6N<$g71PE`~Uy| literal 0 HcmV?d00001 diff --git a/static/icons/locked.png b/static/icons/locked.png new file mode 100755 index 0000000000000000000000000000000000000000..af561c1e16fd4c89dc5dbe7416227de592c86bef GIT binary patch literal 881 zcmV-%1CIQOP)ag2M8cWWCNBiU1|W- zyiQ6=N|BF`kAaDai2=xG`0?Wh!|T_t8SVoC(4e5Qva$;>Jpcg&Hvp)a2Pk=7Sy@@- z*RNj;j~_o~`1)zy6m@c{ye31$*dFjG=eQswvW z-wd~I-MaVr^JfJhn+=3P?3*`lJ_8vHbe#&&D>*QI00G1TV*mv$xwyC(?%uu20Mz7@ zlauoh$*T{Wo10x8K74phK|ukm7A64@K*$D&g8U4$4aC0BfCfNfAlHG^09`8z69)(& za2Px~lJx)hU3G?+A34E(_R~JY!2JI=!~eg3823wDIf1k%1H(T?1_l-mkR&jGelz_2^O>Q^^(#>PH(1M^ z7019dKmdUa0NDgm_~*?k1{QV^21Z66ph8v#22KH>@IMBIZyy={e!9)@>-$rNe}BF) za4P07{Qvb6Yydz2K@0#ok>MZ6re6#nuAT+@lbb<6%ZlOgj&7iSF$Ot1SB4Kat^y5s z&%mo72z2mYhX22Rfeio%AgBSq!QS`}bgF>58UquX2m=G-KZd_Q|1w;D!^t4W@QXn} zS&`w#S9YM+fF(8)Og=00bBSacqJz)ZpL`QswZ|oA zOU3F({X1SPKk~x6XuEsv=8XAw)8^f6+4yAPz9+8P8=W)OPu+UIb^XJ*={IAh-K<&l zaL$g04k_y_qL(?PteLvye)pzF(NnI6PrMd7;adK@Tb|h)ymQu1+Hhz2k(WyjJzsq2 zdCS`S(UY$R^H=O_U9Zd{AvPgH+2>Qg{FGCIEF}EPEKfGc2i>$IdJCCsWznp=MJ7c z+N*Qm@ag0F3LOu)IlDW$I=wxbEQ5lAE*Whq`ozW6<)yH1Q`f3lyV{mb+t$~|*Rg)} z^8AF1l$@k2w##{WiJ7Un$=U4&7w#Rrc=F~^QN;((-aUNu@+t50-#>o+WM^SJ`17FR zLLQ#Phk^!mDIq2uU5y`;A3qATojIZ76gz``gmBZ9{g>N-4pc31jVMV;EJ?LWE=mPb z3`Pb<#<~U;x&|g8hNf1==2j-A+6D$z1_nnZrhGurkei>9nO2Eg!xmQ^AD{*exD6$l jxv9k^iMa*ndMvCA%^;R!2Rl6n>S6G7^>bP0l+XkKPgggY literal 0 HcmV?d00001 diff --git a/static/js/autofill.js b/static/js/autofill.js new file mode 100644 index 0000000..cb3a8b4 --- /dev/null +++ b/static/js/autofill.js @@ -0,0 +1,52 @@ +$(function () { + let name = get_cookie("name"); + let password = get_cookie("password"); + let email = get_cookie("email"); + + if (password === "") { + password = generate_password(); + set_cookie("password", password); + } + + $('input[name="post_name"]').attr("value", name); + $('input[name="post_password"]').attr("value", password); + $('input[name="email"]').attr("value", email); + + function generate_password() { + let chars = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let password_length = 8; + let password = ""; + + for (let i = 0; i <= password_length; i++) { + let random_number = Math.floor(Math.random() * chars.length); + password += chars.substring(random_number, random_number + 1); + } + + return password; + } + + function get_cookie(cname) { + let name = cname + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + + while (c.charAt(0) == " ") { + c = c.substring(1); + } + + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + + return ""; + } + + function set_cookie(cname, cvalue) { + document.cookie = `${cname}=${cvalue};path=/`; + } +}); diff --git a/static/js/captcha.js b/static/js/captcha.js new file mode 100644 index 0000000..2256bfd --- /dev/null +++ b/static/js/captcha.js @@ -0,0 +1,25 @@ +$(function () { + $("#get-captcha").click(function () { + let btn = $(this); + + let board = btn.attr("data-board"); + let reply = btn.attr("data-reply"); + let req_url = `/captcha?board=${board}&reply=${reply}`; + + btn.text("Získat CAPTCHA"); + btn.attr("disabled", true); + btn.addClass("loading"); + + $.get(req_url, function (data, _) { + try { + $("#captcha-id").attr("value", data.id); + $("#captcha").html(``); + } catch { + btn.append(" [Chyba]"); + } + + btn.attr("disabled", false); + btn.removeClass("loading"); + }); + }); +}); diff --git a/static/js/expand.js b/static/js/expand.js new file mode 100644 index 0000000..6dc20ce --- /dev/null +++ b/static/js/expand.js @@ -0,0 +1,103 @@ +$(function () { + $(window).on("setup_post_events", function (event) { + setup_events($(`#${event.id}`).find(".expandable")); + }); + + setup_events($(".expandable")); + + function setup_events(elements) { + elements.each(function() { + $(this).click(function() { + let src_link = $(this).attr("href"); + + let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some( + (ext) => src_link.endsWith(ext) + ); + + if (!is_video) { + toggle_image($(this), src_link); + } else { + toggle_video($(this), src_link); + } + + $(this).toggleClass("expanded"); + + return false; + }) + }) + } + + function toggle_image(parent, src_link) { + let thumb = parent.find(".thumb"); + let src = parent.find(".src"); + + if (src.length === 0) { + thumb.addClass("loading"); + + parent.append(``); + + let src = parent.find(".src"); + + src.hide(); + src.on("load", function () { + thumb.removeClass("loading"); + thumb.hide(); + src.show(); + + parent.closest(".post-files").addClass("float-none-b"); + }); + + return; + } + + thumb.toggle(); + src.toggle(); + + parent.closest(".post-files").toggleClass("float-none-b"); + } + + function toggle_video(parent, src_link) { + let expanded = parent.hasClass("expanded"); + let thumb = parent.find(".thumb"); + let src = parent.parent().find(".src"); + + if (src.length === 0) { + thumb.addClass("loading"); + + parent.append('[Zavřít]
'); + parent + .parent() + .append( + `` + ); + + let src = parent.parent().find(".src"); + + src.hide(); + + src.on("loadstart", function () { + thumb.removeClass("loading"); + thumb.hide(); + src.show(); + src.get(0).play(); + + parent.closest(".post-files").addClass("float-none-b"); + }); + + return; + } + + thumb.toggle(); + src.toggle(); + + if (expanded) { + src.get(0).pause(); + src.get(0).currentTime = 0; + } else { + src.get(0).play(); + } + + parent.closest(".post-files").toggleClass("float-none-b"); + parent.find(".closer").toggle(); + } +}); diff --git a/static/js/hover.js b/static/js/hover.js new file mode 100644 index 0000000..6e29970 --- /dev/null +++ b/static/js/hover.js @@ -0,0 +1,138 @@ +$.fn.isInViewport = function () { + let element_top = $(this).offset().top; + let element_bottom = element_top + $(this).outerHeight(); + let viewport_top = $(window).scrollTop(); + let viewport_bottom = viewport_top + $(window).height(); + + return element_bottom > viewport_top && element_top < viewport_bottom; +}; + +$(function () { + let cache = {}; + let hovering = false; + let preview_w = 0; + let preview_h = 0; + + $(window).on("setup_post_events", function (event) { + setup_events($(`#${event.id}`).find(".quote")); + }); + + setup_events($(".quote")); + + function setup_events(elements) { + elements.on("mouseover", function (event) { + toggle_hover($(this), event); + }); + + elements.on("mouseout", function (event) { + toggle_hover($(this), event); + }); + + elements.on("click", function (event) { + toggle_hover($(this), event); + }); + + elements.on("mousemove", move_preview); + } + + function toggle_hover(quote, event) { + hovering = event.type === "mouseover"; + + if ($("#preview").length !== 0 && !hovering) { + remove_preview(); + return; + } + + let path_segments = quote.prop("pathname").split("/"); + let board = path_segments[2]; + let thread = path_segments[3]; + let id = quote.prop("hash").slice(1); + + let post = $(`#${id}[data-board="${board}"]`); + + if (post.length !== 0 && post.isInViewport()) { + post.toggleClass("highlighted", hovering); + return; + } + + if (post.length !== 0 && hovering) { + create_preview(post.clone(), event.clientX, event.clientY); + return; + } + + let html; + let cached_thread = cache[`${board}/${thread}`]; + + if (cached_thread) { + html = cached_thread[id]; + post = $($.parseHTML(html)); + create_preview(post, event.clientX, event.clientY); + return; + } + + quote.css("cursor", "wait"); + + try { + $.get(`/thread-json/${board}/${thread}`, function (data) { + quote.css("cursor", ""); + cache[`${board}/${thread}`] = data; + html = data[id]; + post = $($.parseHTML(html)); + + create_preview(post, event.clientX, event.clientY); + }); + } catch (e) { + quote.css("cursor", ""); + console.error(e); + } + } + + function move_preview(event) { + position_preview($("#preview"), event.clientX, event.clientY); + } + + function create_preview(preview, x, y) { + if (!hovering) { + return; + } + + preview.attr("id", "preview"); + preview.addClass("box"); + preview.removeClass("highlighted"); + preview.css("position", "fixed"); + + let existing = $("#preview"); + + if (existing.length !== 0) { + existing.replaceWith(preview); + } else { + preview.appendTo("body"); + } + + preview_w = preview.outerWidth(); + preview_h = preview.outerHeight(); + + position_preview(preview, x, y); + + $(window).trigger({ type: "setup_post_events", id: "preview" }); + } + + function remove_preview() { + $("#preview").remove(); + } + + function position_preview(preview, x, y) { + let ww = $(window).width(); + let wh = $(window).height(); + + preview.css("left", `${Math.min(x + 4, ww - preview_w)}px`); + + if (preview_h + y < wh) { + preview.css("top", `${y + 4}px`); + preview.css("bottom", ""); + } else { + preview.css("bottom", `${wh - y + 4}px`); + preview.css("top", ""); + } + } +}); diff --git a/static/js/jquery.min.js b/static/js/jquery.min.js new file mode 100644 index 0000000..43dcd5a --- /dev/null +++ b/static/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).attr({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="

",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0
' + ); + + $("#live-indicator").css("background-color", "orange"); + $("#live-status").text("Připojování..."); + + let thread = window.location.pathname.split("/").slice(-2); + let protocol; + + if (window.location.protocol === "https:") { + protocol = "wss:"; + } else { + protocol = "ws:"; + } + + let last_post = $(".thread").find(".post").last().attr("id"); + + let ws_location = `${protocol}//${window.location.host}/live/${thread[0]}/${thread[1]}/${last_post}`; + let ws = new WebSocket(ws_location); + let interval; + + ws.addEventListener("open", function (_) { + $("#live-indicator").css("background-color", "lime"); + $("#live-status").text("Připojeno pro nové příspěvky"); + + interval = setInterval(function () { + ws.send('{"type":"ping"}'); + }, 10000); + }); + + ws.addEventListener("message", function (msg) { + let data = JSON.parse(msg.data); + + switch (data.type) { + case "created": + $(".thread").append(data.html + "
"); + $(window).trigger({ + type: "setup_post_events", + id: data.id, + }); + break; + case "updated": + $(`#${data.id}`).replaceWith(data.html); + $(window).trigger({ + type: "setup_post_events", + id: data.id, + }); + break; + case "removed": + $(`#${data.id}`).next("br").remove(); + $(`#${data.id}`).remove(); + break; + case "thread_removed": + setTimeout(function () { + $("#live-indicator").css("background-color", "red"); + $("#live-status").text("Vlákno bylo odstraněno"); + }, 100); + break; + default: + break; + } + }); + + ws.addEventListener("close", function (_) { + $("#live-indicator").css("background-color", "red"); + $("#live-status").text("Odpojeno, obnov stránku"); + clearInterval(interval); + }); +}); diff --git a/static/js/post-form.js b/static/js/post-form.js new file mode 100644 index 0000000..b58324c --- /dev/null +++ b/static/js/post-form.js @@ -0,0 +1,173 @@ +$(function () { + $(".open-post-form").click(function () { + $("#post-form").attr("data-visible", true); + + return false; + }); + + $(".close-post-form").click(function () { + if (document.location.hash == "#post-form") { + document.location.hash = ""; + } + + $("#post-form").attr("data-visible", false); + + return false; + }); +}); + +// Stolen and modified code from jschan +$(function () { + let dragging = false; + let x_offset = 0; + let y_offset = 0; + let form = $("#post-form"); + let handle = $("#post-form-handle"); + + let saved_top = window.localStorage.getItem("post_form_top"); + let saved_left = window.localStorage.getItem("post_form_left"); + + if (saved_top) { + form.css("top", saved_top); + } + + if (saved_left) { + form.css("left", saved_left); + form.css("right", "auto"); + } + + handle.on("mousedown", start); + handle.get(0).addEventListener("touchstart", start, { passive: true }); + $(document).on("mouseup", stop); + $(document).on("touchend", stop); + $(window).on("resize", update_max); + $(window).on("orientationchange", update_max); + + function start(event) { + dragging = true; + + const rect = form.get(0).getBoundingClientRect(); + + switch (event.type) { + case "mousedown": + x_offset = event.clientX - rect.left; + y_offset = event.clientY - rect.top; + $(window).on("mousemove", drag); + break; + case "touchstart": + event.preventDefault(); + event.stopPropagation(); + x_offset = event.targetTouches[0].clientX - rect.left; + y_offset = event.targetTouches[0].clientY - rect.top; + $(window).on("touchmove", drag); + break; + default: + break; + } + } + + function drag(event) { + if (!dragging) { + return; + } + + update_max(event); + + switch (event.type) { + case "mousemove": + form.css( + "left", + `${in_bounds( + event.clientX, + x_offset, + form.outerWidth(), + document.documentElement.clientWidth + )}px` + ); + form.css( + "top", + `${in_bounds( + event.clientY, + y_offset, + form.outerHeight(), + document.documentElement.clientHeight + )}px` + ); + break; + case "touchmove": + form.css( + "left", + `${in_bounds( + event.targetTouches[0].clientX, + x_offset, + form.outerWidth(), + document.documentElement.clientWidth + )}px` + ); + form.css( + "top", + `${in_bounds( + event.targetTouches[0].clientY, + y_offset, + form.outerHeight(), + document.documentElement.clientHeight + )}px` + ); + break; + default: + break; + } + + form.css("right", "auto"); + + window.localStorage.setItem("post_form_top", form.css("top")); + window.localStorage.setItem("post_form_left", form.css("left")); + } + + function stop() { + if (dragging) { + dragging = false; + $(window).off("mousemove"); + $(window).off("touchmove"); + } + } + + function update_max() { + let rect = form.get(0).getBoundingClientRect(); + + if (rect.width === 0) { + return; + } + + if (Math.floor(rect.right) > document.documentElement.clientWidth) { + form.css("left", 0); + form.css("right", "auto"); + } + + if (Math.floor(rect.bottom) > document.documentElement.clientHeight) { + form.css("top", 0); + } + + rect = form.get(0).getBoundingClientRect(); + + form.css( + "max-height", + `${document.documentElement.clientHeight - rect.top}px` + ); + + form.css( + "max-width", + `${document.documentElement.clientWidth - rect.left}px` + ); + } + + function in_bounds(pos, offset, size, limit) { + if (pos - offset <= 0) { + return 0; + } else if (pos - offset + size > limit) { + return limit - size; + } else { + return pos - offset; + } + } +}); diff --git a/static/js/quote.js b/static/js/quote.js new file mode 100644 index 0000000..27cc4e3 --- /dev/null +++ b/static/js/quote.js @@ -0,0 +1,36 @@ +$(function () { + let quoted_post = window.localStorage.getItem("quoted_post"); + + if (quoted_post) { + $("#post-form").attr("data-visible", true); + $("#content").append(`>>${quoted_post}\n`); + window.localStorage.removeItem("quoted_post"); + } + + $(window).on("setup_post_events", function (event) { + setup_events($(`#${event.id}`).find(".quote-link")); + }); + + setup_events($(".quote-link")); + + function setup_events(elements) { + elements.each(function () { + $(this).click(function () { + let post_id = $(this).text(); + let thread_url = $(this).attr("data-thread-url"); + let current_url = window.location.pathname; + + if (current_url !== thread_url) { + window.localStorage.setItem("quoted_post", post_id); + window.location.href = `${thread_url}#${post_id}`; + return false; + } + + $("#post-form").attr("data-visible", true); + $("#content").append(`>>${post_id}\n`); + + return false; + }); + }); + } +}); diff --git a/static/js/time.js b/static/js/time.js new file mode 100644 index 0000000..2da69b7 --- /dev/null +++ b/static/js/time.js @@ -0,0 +1,125 @@ +const MINUTE = 60000, + HOUR = 3600000, + DAY = 86400000, + WEEK = 604800000, + MONTH = 2592000000, + YEAR = 31536000000; + +$(function () { + $(window).on("setup_post_events", function (event) { + setup_events($(`#${event.id}`).find("time")); + }); + + setup_events($("time")); + + setInterval(() => { + setup_events($("time")); + }, 60000); + + function setup_events(elements) { + elements.each(function () { + let title = $(this).attr("title"); + + if (!title) { + $(this).attr("title", $(this).text()); + } + + let rel = reltime($(this).attr("datetime")); + + $(this).text(rel); + }); + } + + function reltime(date) { + let delta = Date.now() - Date.parse(date); + let fut = false; + + if (delta < 0) { + delta = Math.abs(delta); + fut = true; + } + + let minutes = Math.floor(delta / MINUTE); + let hours = Math.floor(delta / HOUR); + let days = Math.floor(delta / DAY); + let weeks = Math.floor(delta / WEEK); + let months = Math.floor(delta / MONTH); + let years = Math.floor(delta / YEAR); + + let rt = "Teď"; + + if (minutes > 0) { + if (fut) { + rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`; + } else { + rt = `před ${minutes} ${plural( + "minutou|minutami|minutami", + minutes + )}`; + } + } + + if (hours > 0) { + if (fut) { + rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`; + } else { + rt = `před ${hours} ${plural( + "hodinou|hodinami|hodinami", + hours + )}`; + } + } + + if (days > 0) { + if (fut) { + rt = `za ${days} ${plural("den|dny|dnů", days)}`; + } else { + rt = `před ${days} ${plural("dnem|dny|dny", days)}`; + } + } + + if (weeks > 0) { + if (fut) { + rt = `za ${weeks} ${plural("týden|týdny", weeks)}`; + } else { + rt = `před ${weeks} ${plural("týdnem|týdny", weeks)}`; + } + } + + if (months > 0) { + if (fut) { + rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`; + } else { + rt = `před ${months} ${plural( + "měsícem|měsíci|měsíci", + months + )}`; + } + } + + if (years > 0) { + if (fut) { + rt = `za ${years} ${plural("rok|roky|let", years)}`; + } else { + rt = `před ${years} ${plural("rokem|lety|lety", years)}`; + } + } + + return rt; + } + + function plural(plurals, count) { + let plurals_arr = plurals.split("|"); + let one = plurals_arr[0]; + let few = plurals_arr[1]; + let other = plurals_arr[2]; + + if (count === 1) { + return one; + } else if (count < 5 && count !== 0) { + return few; + } else { + return other; + } + } +}); diff --git a/static/spoiler.png b/static/spoiler.png new file mode 100755 index 0000000000000000000000000000000000000000..9eb148b126661eeed1806758f6dcac1c423e971c GIT binary patch literal 1312 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw3ji**U<|*;%2WC_gPTCzXLg zV`A+@TaUvIGDqX1moA#hS0*5MCSdNGjxLKV(XIf+D_m=>X2kqr^_7|=BJO^0>w^c! zldc{-8X3+W?kMnwp|G?tsAQth|LG!8M=A>L^p}5^D}T>WzxPbY=46=w)6*WRkDSzY zYq7)&9uhbt;oN`3r`Ato@&}pt?;k#X)jPI%&-uTWRa57jWSH+@bGqcP(mjQF_qLRr z`+Y<(Y>LRE;zxnadLDmHl-_)5evY;MuZ?lilHkur1d|qCI94RRdC@{4e?fQA(8V?N z$5LBAi=}UkNj+QrNxZG-)+wXwnS4r(t~!n_43eQ!0@_+7)=Fur1#OZ4{(;}3Zeonl zPOpR~5e}?cD#!W%vG3n)`gyW&UXTV~$-W!kjvENvVU;qy{m*XO+hd#p6>RIaOxqvC zo@VE{e(RPOf&%y0^wves+QT06j^VHWt`v_xkp(Z$KX3eQ{A-!cg>vqa^*i^PelPsR zuw>=xC%gp(96cf$)psA?jeWcKZ}qa9_xUIG7HwYTQS1$j7PcgBcNd2LAh=-f^2tCE z&H|6fVg?31We{epSZZGe6l5>)^mS!_&Lhunpv0XjT>%u5EOCt}an8@pP0cG|a4t$s zEJ;mKD9piwy)A0&eZ#YXAE`Uo~s-mkpLn z0tIeV9GLKZ$*NnqkEL!u{unT89W&5mihxC5|GL-bzMn7VZR?eNUCjJs`RiT5$Fldm zTiuJsBue!Z*q>9@AtXXRP7^Ot8$ zh%!B<`}z0sx_7yW^+oGnZ`=GWU;owVv*+HiUf=xfPv1{renIy&`wGl0*xv-HzTJMS z-b~_u2anKJ;VZWeMx6{X^0>PT#XZw~zlBTcEl>aZERk&XFp1!}!>vsOq|2Y=_RxafJoB8L=S?MfI zhdWA7*D|T}rnGIHyX)6>n@sl?f85wkeE<9Tn*aZ}D{SpzR{KqvpZpTIcwR6rsM+P~ z-&MzMro4SqR2O`p!L~f-oqkS{@!9W9QOECZzGAId;y&+whw?R3<6@iiSu2ie7t2JS z_~XXnq4nm4K+NV7GRMV?_Gh+s%$sdlWPH+ev%S%Av(M31|AN&gzLxsD`&jNh%cHkf zDoj?%E(v$7%KV+YUorlv%Y{Sp@B9;a!3L zDw-Q~Sq--upPV$S^fN={zWlcz`Lfa{shVbQcKsEyJi0b4>;2#9o>Kf1cAih#`fJUV zd%yK3%PjF|PjlU4{yyq`mQd*$%Tv2&l}ufq{Px@PbMJ5bTKnmpg%QUZ83p;)-M_Cp xyWO^0{ch*~_iA^K|J^m6fsvvF2-JMzl>YgLZzDcxEdZ5;44$rjF6*2UngF3zK form:not(#post-form) > .form-table { + margin: 8px auto; +} + +#post-form { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + visibility: hidden; + position: fixed; + right: 0; + top: 0; + background-color: var(--box-color); + padding: 4px; +} + +#post-form:target, +#post-form[data-visible="true"] { + visibility: visible; +} + +#post-form-handle { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: move; +} + +#post-form-handle::after { + content: ""; + display: block; + clear: right; +} + +.edit-box { + display: block; + width: 100%; +} + +.form-table input[type="text"], +.form-table input[type="password"], +.form-table input[type="number"], +.form-table textarea, +.form-table select, +.input-wrapper, +.edit-box { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + display: block; + width: 100%; + color: var(--text); + background-color: var(--input-color); + border-radius: 0; + border: 1px solid var(--input-border); + padding: 4px; +} + +.form-table input[type="checkbox"] { + display: block; + margin: 0 auto; +} + +.form-table input[type="file"] { + width: 100%; +} + +.form-table textarea, +.edit-box { + height: 8rem; + resize: none; +} + +.table-wrap { + overflow: scroll; +} + +.data-table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + margin: 8px 0; +} + +.data-table th { + background-color: var(--table-head); +} + +.data-table td { + background-color: var(--table-background); +} + +.data-table td:not(.form-table td), +.data-table th { + border: 1px solid var(--table-border); + padding: 4px; +} + +.data-table .banner { + margin-top: 0; + margin-bottom: 0; +} + +.news { + margin: 8px 0; +} + +.box { + background-color: var(--box-color); + border-right: 1px solid var(--box-border); + border-bottom: 1px solid var(--box-border); + padding: 8px; +} + +.box:target, +.box.highlighted { + background-color: var(--hl-box-color); + border-right: 1px solid var(--hl-box-border); + border-bottom: 1px solid var(--hl-box-border); +} + +.button { + cursor: pointer; + border-radius: 0; + color: var(--text); + background-color: var(--input-color); + border: 1px solid var(--input-border); + padding: 4px; +} + +.main { + margin: 8px; +} + +.container { + margin: 0 auto; + max-width: 720px; +} + +.title { + color: var(--title-color); + font-family: var(--title-font); + text-align: center; + letter-spacing: -2px; + margin: 0; +} + +.description { + font-weight: bold; + margin: 0; +} + +.big { + font-size: 1.2rem; +} + +.small { + font-size: 0.8rem; +} + +.center { + text-align: center; +} + +.inline-block { + display: inline-block; +} + +.float-r { + float: right; +} + +.float-none-a, +.float-none-b { + float: none !important; +} + +.fixed-table { + table-layout: fixed; +} + +.m-0 { + margin: 0; +} + +.form-table .button, +.full-width { + width: 100%; +} + +.banner { + display: block; + width: 100%; + max-width: 300px; + margin: 8px auto; + border: 1px solid var(--box-border); +} + +.headline { + font-size: 1rem; + margin: 0; +} + +.headline::after { + content: ""; + display: block; + clear: both; +} + +.board-links, +.pagination { + color: var(--link-list-color); +} + +.link-separator::after { + content: " / "; +} + +.link-group::before { + content: " [ "; +} + +.link-group::after { + content: " ] "; +} + +.header { + padding: 2px; +} + +.header::after { + content: ""; + display: block; + clear: both; +} + +.footer { + text-align: center; + font-size: 8pt; + margin-top: 8px; +} + +.post { + margin-bottom: 8px; + padding: 8px; +} + +.post.box { + display: inline-block; + min-width: 400px; +} + +.post:last-of-type { + margin-bottom: 0; +} + +.board-links a, +.pagination a, +.post-number a { + text-decoration: none; +} + +.post-header input[type="checkbox"] { + height: 1em; + vertical-align: middle; + margin: 0; +} + +.catalog-entry { + display: inline-block; + width: 200px; + height: 250px; + overflow: scroll; + margin: 4px; + padding: 8px; +} + +.catalog-entry .thumb { + display: block; + max-width: 100%; + max-height: 50%; + box-shadow: 0 0 3px #000; + margin: 4px auto; + padding: 2px; +} + +.catalog-entry .post-content { + margin: 8px; +} + +.name { + font-weight: bold; + color: var(--name-color); +} + +.tripcode { + color: var(--trip-color); +} + +.capcode { + color: var(--capcode-color); + font-weight: bold; +} + +.user-id { + text-shadow: #000 0 0 1px, #000 0 0 1px, #000 0 0 1px, #000 0 0 1px, + #000 0 0 1px, #000 0 0 1px; + color: #ffffff; + border: 1px solid var(--box-border); + padding: 0 2px; +} + +.post-files { + float: left; + margin: 0 8px 8px 8px; +} + +.post-file { + display: inline-block; + vertical-align: top; + text-align: center; + font-size: 8pt; + padding: 4px; +} + +.thumb { + max-width: 150px; + max-height: 150px; +} + +.post-content { + font-family: inherit; + white-space: pre-wrap; + word-break: break-word; + margin: 0; +} + +.post-content a { + color: var(--post-link-color); +} + +.post-content a:hover { + color: var(--post-link-hover); +} + +.clearfix { + clear: both; +} + +.post .post-content { + margin: 1rem 2rem; +} + +.dead-quote { + color: var(--dead-quote-color); + text-decoration: line-through; +} + +.greentext { + color: var(--greentext-color); +} + +.orangetext { + color: var(--orangetext-color); +} + +.redtext { + color: var(--redtext-color); + font-weight: bold; +} + +.bluetext { + color: var(--bluetext-color); + font-weight: bold; +} + +.glowtext { + text-shadow: 0 0 40px #00fe20, 0 0 2px #00fe20; +} + +.uh-oh-text { + color: var(--uh-oh-text); + background-color: var(--uh-oh-color); +} + +.spoiler { + color: var(--text); + background-color: var(--text); +} + +.spoiler:hover { + background-color: transparent; +} + +.jannytext { + color: var(--jannytext-color); + font-weight: bold; +} + +.icon { + height: 0.8em; + vertical-align: middle; +} + +.posts-omitted { + margin-top: 0; + margin-bottom: 8px; +} + +.board-list { + list-style-type: none; + margin: 0; + padding: 0; +} + +@media only screen and (max-width: 600px) { + .thumb { + max-width: 100px; + max-height: 100px; + } + + .post.box { + display: block; + min-width: auto; + } + + .thread > br { + display: none; + } + + .catalog-entry { + width: 140px; + height: 220px; + } +} + +/* Only in JS */ + +.loading { + opacity: 0.5; + cursor: wait; +} + +#captcha { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + display: block; + width: 100%; + height: 120px; + border: 1px solid var(--input-border); +} + +#captcha img { + display: block; + height: 100%; + image-rendering: pixelated; + margin: 0px auto; +} + +#live-indicator { + display: inline-block; + width: 0.8em; + height: 0.8em; + vertical-align: middle; + border-radius: 50%; +} + +#preview { + -webkit-box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25); + box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25); +} diff --git a/static/themes/yotsuba-b.css b/static/themes/yotsuba-b.css new file mode 100644 index 0000000..68efb52 --- /dev/null +++ b/static/themes/yotsuba-b.css @@ -0,0 +1,39 @@ +:root { + /* General */ + --bg: linear-gradient(#d1d5ee 3rem, #eef2ff 230px); + --text: #000000; + --font: Arial, Helvetica, sans-serif; + /* Text */ + --link-color: #34345c; + --link-hover: #dd0000; + --post-link-color: #34345c; + --post-link-hover: #dd0000; + --link-list-color: #8899aa; + --title-color: #af0a0f; + --title-font: tahoma; + --hr-color: #d3d3d3; + --name-color: #117743; + --trip-color: #117743; + --capcode-color: #cc1105; + /* Tables */ + --table-head: #9988ee; + --table-border: #000000; + --table-background: #ffffff; + /* Forms */ + --input-color: #ffffff; + --input-border: #808080; + /* Box™ */ + --box-color: #d6daf0; + --box-border: #b7c5d9; + --hl-box-color: #d6bad0; + --hl-box-border: #ba9dbf; + /* Formatting */ + --dead-quote-color: #2e2e2e; + --greentext-color: #008000; + --orangetext-color: #ff5100; + --redtext-color: #af0a0f; + --bluetext-color: #0000ff; + --uh-oh-color: #ffffff; + --uh-oh-text: #0038b8; + --jannytext-color: #ff0000; +} diff --git a/static/themes/yotsuba.css b/static/themes/yotsuba.css new file mode 100644 index 0000000..9402a17 --- /dev/null +++ b/static/themes/yotsuba.css @@ -0,0 +1,39 @@ +:root { + /* General */ + --bg: linear-gradient(#fed6af 3rem, #ffffee 230px); + --text: #800000; + --font: Arial, Helvetica, sans-serif; + /* Text */ + --link-color: #800000; + --link-hover: #dd0000; + --post-link-color: #000080; + --post-link-hover: #dd0000; + --link-list-color: #bb8866; + --title-color: #af0a0f; + --title-font: tahoma; + --hr-color: #d9bfb7; + --name-color: #117743; + --trip-color: #117743; + --capcode-color: #cc1105; + /* Tables */ + --table-head: #ffccaa; + --table-border: #800000; + --table-background: #ffffff; + /* Forms */ + --input-color: #ffffff; + --input-border: #808080; + /* Box™ */ + --box-color: #f0e0d6; + --box-border: #d9bfb7; + --hl-box-color: #f0c0b0; + --hl-box-border: #d99f91; + /* Formatting */ + --dead-quote-color: #2e2e2e; + --greentext-color: #008000; + --orangetext-color: #ff5100; + --redtext-color: #af0a0f; + --bluetext-color: #0000ff; + --uh-oh-color: #ffffff; + --uh-oh-text: #0038b8; + --jannytext-color: #ff0000; +} diff --git a/templates/action.html b/templates/action.html new file mode 100644 index 0000000..30fc3d3 --- /dev/null +++ b/templates/action.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}Výsledek{% endblock %} + +{% block content %} +
+

Výsledek

+ + + + + +
Výsledek
+ {% if !response.is_empty() %} + {{ response|linebreaksbr|safe }} + {% else %} + Nic se nezměnilo. + {% endif %} +
+
+{% endblock %} diff --git a/templates/banned.html b/templates/banned.html new file mode 100644 index 0000000..4f56b6b --- /dev/null +++ b/templates/banned.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Máš ban!{% endblock %} + +{% block content %} +
+

Zabanován!!!

+ + + + + +
Máš ban!
+ + Byl jsi zabanován + + {% if let Some(board) = ban.board %} + z /{{ board }}/ + {% else %} + ze všech nástěněk + {% endif %} + + z následujícího důvodu: + +
+ {{ ban.reason }} +
+ Udělil {{ ban.issued_by }} +
+
+ + Tvůj ban platí pro IP adresu/rozsah {{ ban.ip_range }} a + {% if let Some(expires) = ban.expires %} + vyprší + {% else %} + je trvalý. + {% endif %} + + {% if ban.appealable %} +
+
+ {% if let Some(appeal) = ban.appeal %} + Tvé odvolání: +
+ {{ appeal }} +
+ {% else %} + Můžeš se pokusit svůj ban odvolat: +
+ + + + + + + + + +
Odvolání
+ +
+
+ {% endif %} + {% endif %} +
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100755 index 0000000..8638622 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,63 @@ +{% import "./macros/board-links.html" as board_links %} + + + + + + + {% block title %}{% endblock %} + + + + + +
+ +
+ {% block content %}{% endblock %} + +
+
+ + + + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/board-catalog.html b/templates/board-catalog.html new file mode 100755 index 0000000..e4b3657 --- /dev/null +++ b/templates/board-catalog.html @@ -0,0 +1,42 @@ +{% import "./macros/catalog-entry.html" as catalog_entry %} +{% import "./macros/post-actions.html" as post_actions %} + +{% extends "base.html" %} + +{% block theme %}{{ board.config.0.board_theme }}{% endblock %} +{% block title %}Katalog (/{{ board.id }}/){% endblock %} + +{% block content %} +
+
+ +

Katalog (/{{ board.id }}/)

+

{{ board.description }}

+ Index +
+
+
+
+ + + + + + +
+ + + +
+
+
+
+
+ {% for thread in threads %} + {% call catalog_entry::catalog_entry(thread, loop.index, board.config.0.page_size.into()) %} + {% endfor %} +
+
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/board.html b/templates/board.html new file mode 100755 index 0000000..6087069 --- /dev/null +++ b/templates/board.html @@ -0,0 +1,58 @@ +{% import "./macros/pagination.html" as pagination %} +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post-form.html" as post_form %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block theme %}{{ board.config.0.board_theme }}{% endblock %} +{% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %} +{% block scripts %} + + +{% endblock %} + +{% block content %} +
+
+ +

/{{ board.id }}/ - {{ board.name }}

+

{{ board.description }}

+ Katalog +
+
+ [Nové vlákno] +
+
+
+ {% call post_form::post_form(board, false, 0) %} +
+
+
+ {% for (thread, replies) in threads %} +
+ {% call post::post(board, thread, false) %} + {% let count = replies.len() %} + {% if count > 5 %} +

+ {% let omitted = count - 5 %} + {{ "Vynechán|Vynechány|Vynecháno"|czech_plural(omitted) }} {{ omitted }} {{ "příspěvek|příspěvky|příspěvků"|czech_plural(omitted) }}. Zobrazit celé vlákno. +

+ {% for reply_post in replies.iter().rev().take(5).rev() %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} + {% else %} + {% for reply_post in replies %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% call pagination::pagination("/boards/{}"|format(board.id), pages, page) %} +
+ {% call post_actions::post_actions(tcx.perms) %} +
+{% endblock %} diff --git a/templates/edit-posts.html b/templates/edit-posts.html new file mode 100644 index 0000000..7c30598 --- /dev/null +++ b/templates/edit-posts.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Upravit příspěvky{% endblock %} + +{% block content %} +
+

Upravit příspěvky

+
+
+ {% for post in posts %} +
+ Příspěvek #{{ post.id }} na /{{ post.board }}/ + +
+
+ {% endfor %} + +
+
+{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100755 index 0000000..28efc34 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,31 @@ + + + + + + Chyba + {# Error pages are yotsuba (they just are, okay?) #} + {# Go ahead and edit this manually if you actually care #} + + + + +
+
+
+

Je konec...

+ + + + {% if !error_message.is_empty() %} + + {% endif %} +
Chyba {{ error_code }}
{{ error_message }}
+
+ +
+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100755 index 0000000..7538a8d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,65 @@ +{% import "./macros/board-links.html" as board_links %} + +{% extends "base.html" %} + +{% block title %}{{ tcx.cfg.site.name }}{% endblock %} + +{% block content %} +
+
+

{{ tcx.cfg.site.name }}

+

{{ tcx.cfg.site.description }}

+ +
+ {% if let Some(news) = news %} + + + + + + + + + + +
Novinky
+
+

+ {{ news.title }} + + {{ news.author }} - + +

+
+
{{ news.content|safe }}
+
+
+ Zobrazit všechny novinky... +
+ {% endif %} + + + + + + + + + +
NástěnkyStatistika
+ + + {% let board_count = tcx.boards.len() %} + Celkem {{ "byl vytvořen|byly vytvořeny|bylo vytvořeno"|czech_plural(stats.post_count) }} + {{ stats.post_count }} {{ "příspěvek|příspěvky|příspěvků"|czech_plural(stats.post_count) }} + na {{ board_count }} {{ "nástěnce|nástěnkách|nástěnkách"|czech_plural(board_count) }}. +
+ Aktuálně {{ "je nahrán|jsou nahrány|je nahráno"|czech_plural(stats.file_count) }} {{ stats.file_count }} + {{ "soubor|soubory|souborů"|czech_plural(stats.file_count) }}, celkem {{ stats.file_size|filesizeformat }}. +
+
+{% endblock %} diff --git a/templates/ip-posts.html b/templates/ip-posts.html new file mode 100644 index 0000000..9753ea4 --- /dev/null +++ b/templates/ip-posts.html @@ -0,0 +1,29 @@ +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post.html" as post %} +{% import "./macros/static-pagination.html" as static_pagination %} + +{% extends "base.html" %} + +{% block title %}Příspěvky od [{{ ip }}]{% endblock %} + +{% block content %} +
+ +

Příspěvky od [{{ ip }}]

+
+
+
+
+ {% for post in posts %} + Příspěvek z /{{ post.board }}/ +
+ {% call post::post(boards[post.board.as_str()], post, true) %} +
+ {% endfor %} +
+
+ {% call static_pagination::static_pagination("/ip-posts/{}"|format(ip), page, false) %} +
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100755 index 0000000..7f4d12a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Přihlásit se{% endblock %} + +{% block content %} +
+

Přihlásit se

+
+ + + + + + + + + + + + +
Jméno
Heslo
+
+
+{% endblock %} diff --git a/templates/macros/board-links.html b/templates/macros/board-links.html new file mode 100644 index 0000000..1899c21 --- /dev/null +++ b/templates/macros/board-links.html @@ -0,0 +1,8 @@ +{% macro board_links() %} + + {% for board_link in tcx.boards %} + {{ board_link }} + {% if !loop.last %}{% endif %} + {% endfor %} + +{% endmacro %} diff --git a/templates/macros/catalog-entry.html b/templates/macros/catalog-entry.html new file mode 100644 index 0000000..596d17d --- /dev/null +++ b/templates/macros/catalog-entry.html @@ -0,0 +1,23 @@ +{% macro catalog_entry(post, index, page_size) %} +
+ + /{{ post.board }}/{{ post.id }} + {% if let Some(file) = post.files.0.get(0) %} + + + + {% else %} +

[Link]

+ {% endif %} + + R: {{ post.replies }} / P: {{ index|get_page(page_size) }} + {% if post.sticky %} + + {% endif %} + {% if post.locked %} + + {% endif %} + +
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
+{% endmacro %} diff --git a/templates/macros/pagination.html b/templates/macros/pagination.html new file mode 100644 index 0000000..6117257 --- /dev/null +++ b/templates/macros/pagination.html @@ -0,0 +1,25 @@ +{% macro pagination(base, pages, current) %} + +{% endmacro %} diff --git a/templates/macros/post-actions.html b/templates/macros/post-actions.html new file mode 100644 index 0000000..5da477f --- /dev/null +++ b/templates/macros/post-actions.html @@ -0,0 +1,224 @@ +{% macro post_actions() %} +
+ Uživatelské akce + + + + + + + + + + + + + + + + + + + + + +
Odstranit příspěvky +
+ +
+
Odstranit soubory +
+ +
+
Přidat/odstranit spoiler +
+ +
+
Heslo
+ +
+ + + + + + + + +
Důvod hlášení
+ +
+
+
+{% if tcx.perms.owner() || tcx.perms.edit_posts() || tcx.perms.manage_posts() || tcx.perms.reports() || tcx.perms.bans() %} +
+ Uklízečské akce + + {% if tcx.perms.owner() || tcx.perms.manage_posts() %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endif %} + {% if tcx.perms.owner() || tcx.perms.reports() %} + + + + + {% endif %} + {% if tcx.perms.owner() || tcx.perms.bans() %} + + + + + {% if tcx.perms.owner() || tcx.perms.reports() %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + {% endif %} + {% if tcx.perms.owner() || (tcx.perms.edit_posts() && tcx.perms.view_ips()) %} + + + {% endif %} + + + +
Odstranit příspěvky +
+ +
+
Odstranit soubory +
+ +
+
Přidat/odstranit spoiler +
+ +
+
Odstranit od IP na nástěnce +
+ +
+
Odstranit od IP globálně +
+ +
+
Připnout/odepnout +
+ +
+
Uzamknout/odemknout +
+ +
+
Odstranit hlášení +
+ +
+
Zabanovat uživatele +
+ +
+
Zabanovat nahlašovatele +
+ +
+
Globální ban +
+ +
+
Neodvolatelný ban +
+ +
+
Důvod banu
Délka banu (dny, 0 = trvalý)
Rozsah banu + +
Vytrolit uživatele +
+ +
+
+ +
+ {% if tcx.perms.owner() || tcx.perms.edit_posts() %} + + + + +
+ +
+ {% endif %} +
+{% endif %} +{% endmacro %} diff --git a/templates/macros/post-form.html b/templates/macros/post-form.html new file mode 100644 index 0000000..1d1e3ae --- /dev/null +++ b/templates/macros/post-form.html @@ -0,0 +1,105 @@ +{% macro post_form(board, reply, reply_to) %} +
+ + {% if reply %} + + {% endif %} + + + + + + + + + + + + + + + + + {% if !(tcx.perms.bypass_captcha() || tcx.perms.owner()) %} + {% let difficulty %} + {% if reply %} + {% let difficulty = board.config.0.reply_captcha.as_str() %} + {% else %} + {% let difficulty = board.config.0.thread_captcha.as_str() %} + {% endif %} + + {% if (!reply && board.config.0.thread_captcha != "off") || (reply && board.config.0.reply_captcha != "off") %} + + + + + {% endif %} + {% endif %} + + + + + + + + + + + + + + {% if reply %} + + {% else %} + + {% endif %} + +
+ {% if reply %} + Nová odpověď + {% else %} + Nové vlákno + {% endif %} + [X] +
Jméno + +
Email + +
Obsah + +
+ CAPTCHA
+ (vyprší za 10 min.) + +
+
+ + + + + + + +
+ + + +
+
+
+ {% if board.config.0.file_limit > 1 %} + Soubory (max {{ board.config.0.file_limit }}) + {% else %} + Soubor + {% endif %} + +
+ 1 %} multiple="multiple"{% endif %}{% if (!reply && board.config.0.require_thread_file) || (reply && board.config.0.require_reply_file) %} required=""{% endif %}> +
+
Spoiler? +
+ +
+
Heslo
+
+{% endmacro %} diff --git a/templates/macros/post.html b/templates/macros/post.html new file mode 100644 index 0000000..5c77062 --- /dev/null +++ b/templates/macros/post.html @@ -0,0 +1,77 @@ +{% macro post(board, post, boxed) %} +
+
+ + {% if tcx.perms.owner() || tcx.perms.view_ips() %} + [+] + {% endif %} + {% if let Some(email) = post.email %} + {{ post.name }} + {% else %} + {{ post.name }} + {% endif %} + {% if let Some(tripcode) = post.tripcode %} + {{ tripcode }} + {% endif %} + {% if let Some(capcode) = post.capcode %} + ## {{ capcode }} + {% endif %} + {% if tcx.ip == post.ip %} + {# Technically not a tripcode or something but same styling #} + (Ty) + {% endif %} + {% if board.config.0.flags %} + + {% endif %} + + {% if board.config.0.user_ids %} + {{ post.user_id }} + {% endif %} + + Č. + {{ post.id }} + + {% if post.sticky %} + + {% endif %} + {% if post.locked %} + + {% endif %} + + {% if !boxed %} + [Otevřít] + {% endif %} +
+ {% if !post.files.0.is_empty() %} +
+ {% for file in post.files.0 %} +
+ + {% if file.spoiler %} + [Spoiler] + {% else %} + {{ file.original_name|truncate(20) }} + {% endif %} + +
+ ({{ file.size|filesizeformat }}, {{ file.width }}x{{ file.height }}) +
+ +
+ {% endfor %} +
+ {% endif %} +
{{ post.content|add_yous(post.board, tcx.yous)|safe }}
+
+ {% if !post.quotes.is_empty() %} +
+ Odpovědi: + {% for quote in post.quotes %} + >>{{ quote }} + {% endfor %} +
+ {% endif %} +
+{% endmacro %} diff --git a/templates/macros/staff-nav.html b/templates/macros/staff-nav.html new file mode 100644 index 0000000..b05c372 --- /dev/null +++ b/templates/macros/staff-nav.html @@ -0,0 +1,26 @@ +{% macro staff_nav() %} + +{% endmacro %} diff --git a/templates/macros/static-pagination.html b/templates/macros/static-pagination.html new file mode 100644 index 0000000..4ef0118 --- /dev/null +++ b/templates/macros/static-pagination.html @@ -0,0 +1,22 @@ +{% macro static_pagination(base, current, chain) %} + +{% endmacro %} diff --git a/templates/news.html b/templates/news.html new file mode 100644 index 0000000..36a6a6e --- /dev/null +++ b/templates/news.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Novinky{% endblock %} + +{% block content %} +
+
+ +

Novinky

+
+ {% for newspost in news %} +
+

+ {{ newspost.title }} + + {{ newspost.author }} - + +

+
+
{{ newspost.content|safe }}
+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/overboard-catalog.html b/templates/overboard-catalog.html new file mode 100755 index 0000000..0ac4dd6 --- /dev/null +++ b/templates/overboard-catalog.html @@ -0,0 +1,40 @@ +{% import "./macros/catalog-entry.html" as catalog_entry %} +{% import "./macros/post-actions.html" as post_actions %} + +{% extends "base.html" %} + +{% block title %}Katalog nadnástěnky{% endblock %} + +{% block content %} +
+
+ +

Katalog nadnástěnky

+

Nově naťuknutá vlákna ze všech nástěnek

+ Index +
+
+
+
+ + + + + +
+ + + +
+
+
+
+
+ {% for thread in threads %} + {% call catalog_entry::catalog_entry(thread, loop.index, crate::GENERIC_PAGE_SIZE) %} + {% endfor %} +
+
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/overboard.html b/templates/overboard.html new file mode 100644 index 0000000..d31a652 --- /dev/null +++ b/templates/overboard.html @@ -0,0 +1,47 @@ +{% import "./macros/pagination.html" as pagination %} +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block title %}Index nadnástěnky{% endblock %} + +{% block content %} +
+
+ +

Index nadnástěnky

+

Nově naťuknutá vlákna ze všech nástěnek

+ Katalog +
+
+
+
+ {% for (thread, replies) in threads %} +
+ Vlákno z /{{ thread.board }}/ + {% call post::post(boards[thread.board.as_str()], thread, false) %} + {% let count = replies.len() %} + {% if count > 5 %} +

+ {% let omitted = count - 5 %} + {{ "Vynechán|Vynechány|Vynecháno"|czech_plural(omitted) }} {{ omitted }} {{ "příspěvek|příspěvky|příspěvků"|czech_plural(omitted) }}. Zobrazit celé vlákno. +

+ {% for reply_post in replies.iter().rev().take(5).rev() %} + {% call post::post(boards[reply_post.board.as_str()], reply_post, true) %} +
+ {% endfor %} + {% else %} + {% for reply_post in replies %} + {% call post::post(boards[reply_post.board.as_str()], reply_post, true) %} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% call pagination::pagination("/overboard", pages, page) %} +
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/page.html b/templates/page.html new file mode 100644 index 0000000..beb6595 --- /dev/null +++ b/templates/page.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} +{% block title %}{{ tcx.cfg.site.name }} ({{ name }}){% endblock %} +{% block content %}{{ content|safe }}{% endblock %} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..b958392 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,47 @@ +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post.html" as post %} +{% import "./macros/static-pagination.html" as static_pagination %} + +{% extends "base.html" %} + +{% block title %} +Vyhledávání ({% if let Some(board) = board_opt %}/{{ board.id }}/{% else %}nadnástěnka{% endif %}) +{% endblock %} + +{% block content %} +
+ +

+ Výsledky pro "{{ query }}" ( + {% if let Some(board) = board_opt %} + /{{ board.id }}/ + {% else %} + nadnástěnka + {% endif %} + ) +

+
+
+
+
+ {% for post in posts %} + {% if let Some(board) = board_opt %} + {% call post::post(board, post, true) %} + {% else %} + Příspěvek z /{{ post.board }}/ +
+ {% call post::post(boards[post.board.as_str()], post, true) %} + {% endif %} +
+ {% endfor %} +
+
+ {% if let Some(board) = board_opt %} + {% call static_pagination::static_pagination("/search?board={}&query={}"|format(board.id, query|urlencode_strict), page, true) %} + {% else %} + {% call static_pagination::static_pagination("/search?query={}"|format(query|urlencode_strict), page, true) %} + {% endif %} +
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/staff/account.html b/templates/staff/account.html new file mode 100755 index 0000000..2e67809 --- /dev/null +++ b/templates/staff/account.html @@ -0,0 +1,67 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Účet ({{ account.username }}){% endblock %} + +{% block content %} +

Účet ({{ account.username }})

+{% call staff_nav::staff_nav() %} +
+

Změnit heslo

+
+ + + + + + + + + + + + +
Staré heslo
Nové heslo
+
+
+{% if tcx.perms.owner() %} +

Předat vlastnictví

+
+ + + + + + + + + + + + +
Účet
Potvrdit +
+ +
+
+
+
+{% endif %} +

Vymazat účet

+
+ + + + + + + + +
Potvrdit +
+ +
+
+
+{% endblock %} diff --git a/templates/staff/accounts.html b/templates/staff/accounts.html new file mode 100755 index 0000000..f898bb6 --- /dev/null +++ b/templates/staff/accounts.html @@ -0,0 +1,56 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Účty{% endblock %} + +{% block content %} +

Účty

+{% call staff_nav::staff_nav() %} +
+

Účty

+
+
+ + + + + + + + + {% for account in accounts %} + + + + + + + + {% endfor %} +
JménoVlastníkVytvořenOprávnění
{{ account.username }}{% if account.owner %}Ano{% else %}Ne{% endif %}{{ account.permissions.0 }} [Zobrazit]
+
+ {% if tcx.perms.owner() %} + + {% endif %} +
+{% if tcx.perms.owner() %} +
+

Vytvořit účet

+
+ + + + + + + + + + + + +
Jméno
Heslo
+
+{% endif %} +{% endblock %} diff --git a/templates/staff/banners.html b/templates/staff/banners.html new file mode 100755 index 0000000..1da830b --- /dev/null +++ b/templates/staff/banners.html @@ -0,0 +1,45 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Bannery{% endblock %} + +{% block content %} +

Bannery

+{% call staff_nav::staff_nav() %} +
+

Bannery

+
+
+ + + + + + {% for banner in banners %} + + + + + {% endfor %} +
Banner
+ +
+
+ + +
+
+

Přidat bannery

+
+ + + + + + + + +
Bannery
+
+{% endblock %} diff --git a/templates/staff/bans.html b/templates/staff/bans.html new file mode 100755 index 0000000..602a189 --- /dev/null +++ b/templates/staff/bans.html @@ -0,0 +1,54 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Bany{% endblock %} + +{% block content %} +

Bany

+{% call staff_nav::staff_nav() %} +
+

Bany

+
+
+ + + + + + + + + + + + + {% for ban in bans %} + + + + + + + + + + {% if let Some(expires) = ban.expires %} + + {% else %} + + {% endif %} + + {% endfor %} +
IPNástěnkaDůvodUdělilOdvolatelnýOdvoláníUdělěnVyprší
+ {% if ban.ip_range.network() == ban.ip_range.broadcast() %} + {{ ban.ip_range.ip() }} + {% else %} + {{ ban.ip_range.network() }}-{{ ban.ip_range.broadcast() }} + {% endif %} + {% if let Some(board) = ban.board %}/{{ board }}/{% else %}Všechny{% endif %}
{{ ban.reason }}
{{ ban.issued_by }}{% if ban.appealable %}Ano{% else %}Ne{% endif %}{% if let Some(appeal) = ban.appeal %}
{{ appeal }}
{% else %}-{% endif %}
Nikdy
+
+ + +
+{% endblock %} diff --git a/templates/staff/board-config.html b/templates/staff/board-config.html new file mode 100755 index 0000000..6946050 --- /dev/null +++ b/templates/staff/board-config.html @@ -0,0 +1,187 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Nastavení (/{{ board.id }}/){% endblock %} + +{% block content %} +

Nastavení (/{{ board.id }}/)

+{% call staff_nav::staff_nav() %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Uzamknout nástěnku +
+ +
+
Výchozí jméno
Velikost stránky
Počet stránek
Limit souborů
Limit naťuknutí
Limit odpovědí
Identifikátory (anti-samefag) +
+ +
+
Vlajky +
+ +
+
CAPTCHA (vlákno) + +
CAPTCHA (odpověď) + +
Motiv nástěnky
Vyžadovat obsah ve vlákně +
+ +
+
Vyžadovat soubor ve vlákně +
+ +
+
Vyžadovat obsah v odpovědi +
+ +
+
Vyžadovat soubor v odpovědi +
+ +
+
Antispam +
+ +
+
Interval antispamu (IP)
Interval antispamu (Obsah)
Interval antispamu (IP+Obsah)
Interval mezi vlákny (IP)
+
+{% endblock %} diff --git a/templates/staff/boards.html b/templates/staff/boards.html new file mode 100755 index 0000000..935adb4 --- /dev/null +++ b/templates/staff/boards.html @@ -0,0 +1,75 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Nástěnky{% endblock %} + +{% block content %} +

Nástěnky

+{% call staff_nav::staff_nav() %} +
+

Nástěnky

+
+
+ + + + + + + + + + {% for board in boards %} + + + + + + + + + {% endfor %} +
IDJménoPopisVytvořenaNastavení
/{{ board.id }}/{{ board.name }}{{ board.description }}{% if tcx.perms.owner() || tcx.perms.board_config() %}[Zobrazit]{% else %}-{% endif %}
+
+ + {% if tcx.perms.owner() %} + +
+ + + + + + + + + + + + +
Jméno
Popis
+ {% endif %} +
+
+

Vytvořit nástěnku

+
+ + + + + + + + + + + + + + + + +
ID
Jméno
Popis
+
+{% endblock %} diff --git a/templates/staff/edit-news.html b/templates/staff/edit-news.html new file mode 100644 index 0000000..1c8104e --- /dev/null +++ b/templates/staff/edit-news.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Upravit příspěvky{% endblock %} + +{% block content %} +
+

Upravit novinky

+
+
+ {% for newspost in news %} +
+

+ {{ newspost.title }} + + {{ newspost.author }} - + +

+
+ +
+
+ {% endfor %} + +
+
+{% endblock %} diff --git a/templates/staff/news.html b/templates/staff/news.html new file mode 100644 index 0000000..a8c5c42 --- /dev/null +++ b/templates/staff/news.html @@ -0,0 +1,51 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Novinky{% endblock %} + +{% block content %} +

Novinky

+{% call staff_nav::staff_nav() %} +
+

Novinky

+
+
+ + + + + + + + {% for newspost in news %} + + + + + + + {% endfor %} +
TitulekAutorDatum
{{ newspost.title }}{{ newspost.author }}
+
+ + +
+
+

Vytvořit novinky

+
+ + + + + + + + + + + + +
Titulek
Obsah
+
+{% endblock %} diff --git a/templates/staff/permissions.html b/templates/staff/permissions.html new file mode 100755 index 0000000..842df3c --- /dev/null +++ b/templates/staff/permissions.html @@ -0,0 +1,179 @@ +{% import "../macros/staff-nav.html" as staff_nav %} + +{% extends "base.html" %} + +{% block title %}Oprávnění ({{ account.username }}){% endblock %} + +{% block content %} +

Oprávnění ({{ account.username }})

+{% call staff_nav::staff_nav() %} +
+{% if account.perms().owner() %} +

Tento uživatel je vlastník, změny nebudou mít žádný vliv.

+
+{% endif %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if tcx.perms.owner() %} + + + + {% endif %} +
Upravit příspěvky +
+ +
+
Spravovat příspěvky +
+ +
+
Používat capcode +
+ +
+
Vlastní capcode +
+ +
+
Záznamy +
+ +
+
Hlášení +
+ +
+
Bany +
+ +
+
Bannery +
+ +
+
Nastavení nástěnek +
+ +
+
Novinky +
+ +
+
Uklízečtext (pravý redtext) +
+ +
+
Zobrazit IP adresy +
+ +
+
Obejít ban +
+ +
+
Obejít uzamčení nástěnky +
+ +
+
Obejít uzamčení vlákna +
+ +
+
Obejít CAPTCHA +
+ +
+
Obejít antispam +
+ +
+
+
+{% endblock %} diff --git a/templates/staff/reports.html b/templates/staff/reports.html new file mode 100755 index 0000000..bb643e9 --- /dev/null +++ b/templates/staff/reports.html @@ -0,0 +1,40 @@ +{% import "../macros/post-actions.html" as post_actions %} +{% import "../macros/post.html" as post %} +{% import "../macros/staff-nav.html" as staff_nav %} +{% import "../macros/static-pagination.html" as static_pagination %} + +{% extends "base.html" %} + +{% block title %}Hlášení{% endblock %} + +{% block content %} +
+

Hlášení

+

>Ukliď to!!!

+
+{% call staff_nav::staff_nav() %} +
+
+ {% for post in posts %} + Příspěvek z /{{ post.board }}/ +
+ {% call post::post(boards[post.board.as_str()], post, true) %} + + + + + + {% for report in post.reports.0 %} + + + + + {% endfor %} +
IP adresaDůvod hlášení
{{ report.reporter_ip }} (){{ report.reason }}
+
+ {% endfor %} + {% call static_pagination::static_pagination("/staff/reports", page, false) %} +
+ {% call post_actions::post_actions() %} +
+{% endblock %} diff --git a/templates/thread.html b/templates/thread.html new file mode 100644 index 0000000..5c715b9 --- /dev/null +++ b/templates/thread.html @@ -0,0 +1,55 @@ +{% import "./macros/post-actions.html" as post_actions %} +{% import "./macros/post-form.html" as post_form %} +{% import "./macros/post.html" as post %} + +{% extends "base.html" %} + +{% block theme %}{{ board.config.0.board_theme }}{% endblock %} +{% block title %}/{{ board.id }}/ - {{ thread.content_nomarkup|inline_post }}{% endblock %} + +{% block scripts %} + + + +{% endblock %} + +{% block content %} +
+
+ +

/{{ board.id }}/ - {{ board.name }}

+

{{ board.description }}

+ Katalog +
+
+ [Nová odpověď] +
+
+
+ {% call post_form::post_form(board, true, thread.id) %} +
+
+ +
+
+
+ {% call post::post(board, thread, false) %} + {% for reply_post in replies %} + {% call post::post(board, reply_post, true) %} +
+ {% endfor %} +
+
+ +
+ {% call post_actions::post_actions() %} +
+{% endblock %}