jake kalstad 2 年之前
当前提交
bfe30bdfcc
共有 100 个文件被更改,包括 19385 次插入0 次删除
  1. 4 0
      .env
  2. 7 0
      .gitignore
  3. 3 0
      .vscode/settings.json
  4. 4939 0
      Cargo.lock
  5. 41 0
      Cargo.toml
  6. 48 0
      Dockerfile
  7. 8 0
      SPECS/muzli-colors.svg
  8. 1 0
      assets/css/calendar.js.min.css
  9. 0 0
      assets/css/mini-default.min.css
  10. 533 0
      assets/css/styles.css
  11. 0 0
      assets/css/styles.min.css
  12. 二进制
      assets/favicon.ico
  13. 二进制
      assets/favicon.png
  14. 61 0
      assets/images/akaunting-logo-horizontal.svg
  15. 二进制
      assets/images/bot_dark.webp
  16. 二进制
      assets/images/logo_transparent.png
  17. 8 0
      assets/images/matrix-logo-white.svg
  18. 二进制
      assets/images/matrix.png
  19. 22 0
      assets/images/sso/apple.svg
  20. 二进制
      assets/images/sso/beta-jesse-lee-peterson.gif
  21. 12 0
      assets/images/sso/facebook.svg
  22. 1 0
      assets/images/sso/github.svg
  23. 23 0
      assets/images/sso/gitlab.svg
  24. 5 0
      assets/images/sso/google.svg
  25. 二进制
      assets/images/sso/user_password.svg
  26. 74 0
      assets/js/app.js
  27. 222 0
      assets/js/calendar.min.js
  28. 6 0
      assets/js/easymde@2.18.0.min.js
  29. 1 0
      assets/js/sheetjs-0.20.0.full.min.js
  30. 1 0
      assets/langs/en.js
  31. 1 0
      assets/langs/es.js
  32. 1 0
      deploy.sh
  33. 3 0
      kinbot/.vscode/settings.json
  34. 7 0
      kinbot/Cargo.lock
  35. 8 0
      kinbot/Cargo.toml
  36. 3 0
      kinbot/matrix_bot/.env
  37. 1 0
      kinbot/matrix_bot/.gitignore
  38. 3 0
      kinbot/matrix_bot/.vscode/settings.json
  39. 3238 0
      kinbot/matrix_bot/Cargo.lock
  40. 19 0
      kinbot/matrix_bot/Cargo.toml
  41. 6 0
      kinbot/matrix_bot/readme.md
  42. 87 0
      kinbot/matrix_bot/src/main.rs
  43. 3 0
      kinbot/src/main.rs
  44. 二进制
      logo_v1/banner.png
  45. 二进制
      logo_v1/facebook_cover_photo_1.png
  46. 二进制
      logo_v1/facebook_cover_photo_2.png
  47. 二进制
      logo_v1/facebook_profile_image.png
  48. 二进制
      logo_v1/favicon.png
  49. 二进制
      logo_v1/instagram_profile_image.png
  50. 二进制
      logo_v1/linkedin_banner_image_1.png
  51. 二进制
      logo_v1/linkedin_banner_image_2.png
  52. 二进制
      logo_v1/linkedin_profile_image.png
  53. 二进制
      logo_v1/logo.png
  54. 二进制
      logo_v1/logo_transparent.png
  55. 二进制
      logo_v1/pinterest_board_photo.png
  56. 二进制
      logo_v1/pinterest_profile_image.png
  57. 二进制
      logo_v1/twitter_header_photo_1.png
  58. 二进制
      logo_v1/twitter_header_photo_2.png
  59. 二进制
      logo_v1/twitter_profile_image.png
  60. 二进制
      logo_v1/youtube_profile_image.png
  61. 49 0
      marketing.md
  62. 二进制
      marketing.pdf
  63. 116 0
      readme.md
  64. 1200 0
      src/akaunting.rs
  65. 360 0
      src/board.rs
  66. 25 0
      src/common.rs
  67. 1195 0
      src/entity.rs
  68. 584 0
      src/file.rs
  69. 244 0
      src/home.rs
  70. 204 0
      src/main.rs
  71. 712 0
      src/matrix.rs
  72. 333 0
      src/milestone.rs
  73. 385 0
      src/note.rs
  74. 299 0
      src/organization.rs
  75. 379 0
      src/project.rs
  76. 425 0
      src/service_item.rs
  77. 471 0
      src/task.rs
  78. 914 0
      src/user.rs
  79. 6 0
      start-gpt.sh
  80. 7 0
      start-matrix-bot.sh
  81. 235 0
      tables.sql
  82. 178 0
      templates/akaunting.html
  83. 181 0
      templates/board.html
  84. 0 0
      templates/client_site/about.html
  85. 0 0
      templates/client_site/contact.html
  86. 53 0
      templates/client_site/home.html
  87. 55 0
      templates/client_site/layout.html
  88. 0 0
      templates/client_site/services.html
  89. 233 0
      templates/contact.html
  90. 234 0
      templates/dashboard.html
  91. 130 0
      templates/documentation.html
  92. 203 0
      templates/entity.html
  93. 84 0
      templates/file.html
  94. 138 0
      templates/home.html
  95. 33 0
      templates/invoice_list.html
  96. 80 0
      templates/layout.html
  97. 42 0
      templates/login.html
  98. 42 0
      templates/login_username.html
  99. 93 0
      templates/milestone.html
  100. 63 0
      templates/note.html

+ 4 - 0
.env

@@ -0,0 +1,4 @@
+DATABASE_URL=postgres://cloudify2:cloudify@localhost:5432/projectmanager
+S3_URL=localhost:9000
+TIDE_SECRET=its_a_big_secret_now
+JWT_SECRET=oh_dear_jwt_secrets_go_here

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+/target
+/kinbot/kinbot-gpt/results_modified
+/kinbot/kinbot-gpt
+/kinbot/target
+/kinbot/matrix_bot/target
+/deploy/sh
+/nginx.conf

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "git-blame.gitWebUrl": "",
+}

+ 4939 - 0
Cargo.lock

@@ -0,0 +1,4939 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aead"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "aead"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "aes"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
+dependencies = [
+ "aes-soft",
+ "aesni",
+ "cipher 0.2.5",
+]
+
+[[package]]
+name = "aes"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cipher 0.3.0",
+ "cpufeatures",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cipher 0.4.4",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
+dependencies = [
+ "aead 0.3.2",
+ "aes 0.6.0",
+ "cipher 0.2.5",
+ "ctr 0.6.0",
+ "ghash 0.3.1",
+ "subtle",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6"
+dependencies = [
+ "aead 0.4.3",
+ "aes 0.7.5",
+ "cipher 0.3.0",
+ "ctr 0.8.0",
+ "ghash 0.4.4",
+ "subtle",
+]
+
+[[package]]
+name = "aes-soft"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
+dependencies = [
+ "cipher 0.2.5",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aesni"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
+dependencies = [
+ "cipher 0.2.5",
+ "opaque-debug",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom 0.2.8",
+ "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 = "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 = "anyhow"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+dependencies = [
+ "serde",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e80b5ad1afe82872b7aa3e9de9b206ecb85584aa324f0f60fa4c903ce935936b"
+dependencies = [
+ "basic-toml",
+ "mime",
+ "mime_guess",
+ "nom",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "serde",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+
+[[package]]
+name = "assign"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"
+
+[[package]]
+name = "async-channel"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-compression"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
+dependencies = [
+ "brotli",
+ "flate2",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-dup"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c"
+dependencies = [
+ "futures-io",
+ "simple-mutex",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+ "tokio",
+]
+
+[[package]]
+name = "async-h1"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7"
+dependencies = [
+ "async-channel",
+ "async-dup",
+ "async-std",
+ "futures-core",
+ "http-types",
+ "httparse",
+ "log",
+ "pin-project",
+]
+
+[[package]]
+name = "async-io"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794"
+dependencies = [
+ "async-lock",
+ "autocfg 1.1.0",
+ "concurrent-queue",
+ "futures-lite",
+ "libc",
+ "log",
+ "parking",
+ "polling",
+ "slab",
+ "socket2 0.4.9",
+ "waker-fn",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-mutex"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-once-cell"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72faff1fdc615a0199d7bf71e6f389af54d46a66e9beb5d76c39e48eda93ecce"
+
+[[package]]
+name = "async-process"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "autocfg 1.1.0",
+ "blocking",
+ "cfg-if 1.0.0",
+ "event-listener",
+ "futures-lite",
+ "libc",
+ "signal-hook",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "async-session"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f"
+dependencies = [
+ "anyhow",
+ "async-std",
+ "async-trait",
+ "base64 0.12.3",
+ "bincode",
+ "blake3 0.3.8",
+ "chrono",
+ "hmac 0.8.1",
+ "kv-log-macro",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "sha2 0.9.9",
+]
+
+[[package]]
+name = "async-sse"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10"
+dependencies = [
+ "async-channel",
+ "async-std",
+ "http-types",
+ "log",
+ "memchr",
+ "pin-project-lite 0.1.12",
+]
+
+[[package]]
+name = "async-std"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+dependencies = [
+ "async-channel",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[package]]
+name = "async-task"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
+
+[[package]]
+name = "async-trait"
+version = "0.1.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "atoi"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599"
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backoff"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
+dependencies = [
+ "futures-core",
+ "getrandom 0.2.8",
+ "instant",
+ "pin-project-lite 0.2.13",
+ "rand 0.8.5",
+ "tokio",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base-x"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
+
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "blake3"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.5.2",
+ "cc",
+ "cfg-if 0.1.10",
+ "constant_time_eq 0.1.5",
+ "crypto-mac 0.8.0",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "blake3"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.7.4",
+ "cc",
+ "cfg-if 1.0.0",
+ "constant_time_eq 0.2.6",
+ "digest 0.10.7",
+]
+
+[[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 = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+]
+
+[[package]]
+name = "blowfish"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b"
+dependencies = [
+ "byteorder",
+ "cipher 0.2.5",
+ "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.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[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 = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chacha20"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f08493fa7707effc63254c66c6ea908675912493cd67952eda23c09fae2610b1"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cipher 0.3.0",
+ "cpufeatures",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cipher 0.3.0",
+ "cpufeatures",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6547abe025f4027edacd9edaa357aded014eecec42a5070d9b885c3c334aba2"
+dependencies = [
+ "aead 0.4.3",
+ "chacha20 0.7.3",
+ "cipher 0.3.0",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5"
+dependencies = [
+ "aead 0.4.3",
+ "chacha20 0.8.2",
+ "cipher 0.3.0",
+ "poly1305",
+ "zeroize",
+]
+
+[[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",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
+
+[[package]]
+name = "const_fn"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
+
+[[package]]
+name = "cookie"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951"
+dependencies = [
+ "aes-gcm 0.8.0",
+ "base64 0.13.1",
+ "hkdf 0.10.0",
+ "hmac 0.10.1",
+ "percent-encoding",
+ "rand 0.8.5",
+ "sha2 0.9.9",
+ "time 0.2.27",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpuid-bool"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
+
+[[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 1.0.0",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[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 = "crypto-mac"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "csrf"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0257f83673d49bb1f149d923cb923f7d5a270248d64fdb6e847190935ecf1e0"
+dependencies = [
+ "aead 0.4.3",
+ "aes-gcm 0.9.4",
+ "byteorder",
+ "chacha20poly1305 0.8.2",
+ "chrono",
+ "data-encoding",
+ "generic-array",
+ "hmac 0.11.0",
+ "log",
+ "rand 0.8.5",
+ "sha2 0.9.9",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ctr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f"
+dependencies = [
+ "cipher 0.2.5",
+]
+
+[[package]]
+name = "ctr"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
+dependencies = [
+ "cipher 0.3.0",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "rand_core 0.5.1",
+ "serde",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "scratch",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[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 1.0.68",
+ "quote 1.0.33",
+ "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 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+dependencies = [
+ "cfg-if 1.0.0",
+ "hashbrown 0.14.0",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core 0.9.8",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
+
+[[package]]
+name = "der"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c"
+dependencies = [
+ "const-oid",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
+dependencies = [
+ "darling",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
+dependencies = [
+ "derive_builder_core",
+ "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",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "serde",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand 0.7.3",
+ "serde",
+ "serde_bytes",
+ "sha2 0.9.9",
+ "zeroize",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
+[[package]]
+name = "erased-serde"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[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 = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "femme"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[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 = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[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.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-lite"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite 0.2.13",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[package]]
+name = "futures-signals"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a12cb78961d5c0bc0e358599bba98ec09201090a22339cd8ea27e815c11b25"
+dependencies = [
+ "discard",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "gensym",
+ "pin-project",
+]
+
+[[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-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite 0.2.13",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gensym"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb328fe25cbf075818a3e57bb5ee39b49b4f26c94d685356426154c5962cccd"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+ "uuid 0.7.4",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ghash"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
+dependencies = [
+ "opaque-debug",
+ "polyval 0.4.5",
+]
+
+[[package]]
+name = "ghash"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99"
+dependencies = [
+ "opaque-debug",
+ "polyval 0.5.3",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[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.2",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "hashlink"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
+dependencies = [
+ "hashbrown 0.12.3",
+]
+
+[[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.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
+dependencies = [
+ "digest 0.9.0",
+ "hmac 0.10.1",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
+dependencies = [
+ "digest 0.9.0",
+ "hmac 0.11.0",
+]
+
+[[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.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+dependencies = [
+ "crypto-mac 0.8.0",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac 0.10.1",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac 0.11.1",
+ "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 = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite 0.2.13",
+]
+
+[[package]]
+name = "http-client"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5"
+dependencies = [
+ "async-trait",
+ "cfg-if 1.0.0",
+ "http-types",
+ "log",
+]
+
+[[package]]
+name = "http-types"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
+dependencies = [
+ "anyhow",
+ "async-channel",
+ "async-std",
+ "base64 0.13.1",
+ "cookie",
+ "futures-lite",
+ "infer",
+ "pin-project-lite 0.2.13",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "serde_qs",
+ "serde_urlencoded",
+ "url",
+]
+
+[[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 = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite 0.2.13",
+ "socket2 0.4.9",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[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.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexed_db_futures"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26ac735f676c52305becf53264b91cea9866a8de61ccbf464405b377b9cbca9"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "uuid 0.8.2",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+dependencies = [
+ "autocfg 1.1.0",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[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 = "infer"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76e86b86ae312accbf05ade23ce76b625e0e47a255712b7414037385a1c05380"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "js_int"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "js_option"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "8.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
+dependencies = [
+ "base64 0.21.4",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
+[[package]]
+name = "kinbrio"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "askama",
+ "async-std",
+ "async-trait",
+ "base64 0.21.4",
+ "chrono",
+ "dotenv",
+ "jsonwebtoken",
+ "lazy_static",
+ "markdown",
+ "matrix-sdk",
+ "minio-rsc",
+ "multer",
+ "pwhash",
+ "reqwest",
+ "serde",
+ "serde-this-or-that",
+ "serde_derive",
+ "serde_json",
+ "sqlx",
+ "strum",
+ "strum_macros",
+ "tide",
+ "tide-compress",
+ "tide-csrf",
+ "tokio",
+ "url",
+ "uuid 1.4.1",
+ "value-bag 0.1.0",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
+
+[[package]]
+name = "libm"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg 1.1.0",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "serde",
+ "value-bag 1.0.0-alpha.9",
+]
+
+[[package]]
+name = "lru"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909"
+dependencies = [
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "markdown"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef3aab6a1d529b112695f72beec5ee80e729cb45af58663ec902c8fac764ecdd"
+dependencies = [
+ "lazy_static",
+ "pipeline",
+ "regex",
+]
+
+[[package]]
+name = "matrix-sdk"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbeafb4809f33f377165f2fbcf10e0613053ad206762194c3050a727fd3abcb2"
+dependencies = [
+ "anymap2",
+ "async-once-cell",
+ "async-stream",
+ "async-trait",
+ "backoff",
+ "bytes",
+ "dashmap",
+ "derive_builder",
+ "event-listener",
+ "futures-core",
+ "futures-signals",
+ "futures-util",
+ "http",
+ "matrix-sdk-base",
+ "matrix-sdk-common",
+ "matrix-sdk-indexeddb",
+ "matrix-sdk-sled",
+ "mime",
+ "reqwest",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "url",
+ "wasm-timer",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-base"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b944f6d1fc8779ba790dd0b942ceff45c626c1f5da847f01122d355ad06511bd"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "dashmap",
+ "futures-channel",
+ "futures-core",
+ "futures-signals",
+ "futures-util",
+ "lru",
+ "matrix-sdk-common",
+ "matrix-sdk-crypto",
+ "once_cell",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-common"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b85a6a743cc9dcf9385e61a26db78276078beddd27f3762d9d82baa2030695f1"
+dependencies = [
+ "async-lock",
+ "futures-core",
+ "futures-util",
+ "instant",
+ "ruma",
+ "serde",
+ "tokio",
+ "wasm-bindgen-futures",
+ "wasm-timer",
+]
+
+[[package]]
+name = "matrix-sdk-crypto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68fa699e8dd54578a4b92e3fcd18a50da8e415a0c042da1706b0330fc2d8f949"
+dependencies = [
+ "aes 0.8.3",
+ "async-trait",
+ "atomic",
+ "base64 0.13.1",
+ "byteorder",
+ "ctr 0.9.2",
+ "dashmap",
+ "event-listener",
+ "futures-util",
+ "hmac 0.12.1",
+ "matrix-sdk-common",
+ "pbkdf2",
+ "rand 0.8.5",
+ "ruma",
+ "serde",
+ "serde_json",
+ "sha2 0.10.8",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "vodozemac",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-indexeddb"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7847d36bba832bc787214323bc042b71dca7fdf2aee9f0e3eb573b64f2f7eb7f"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.13.1",
+ "dashmap",
+ "derive_builder",
+ "getrandom 0.2.8",
+ "indexed_db_futures",
+ "js-sys",
+ "matrix-sdk-base",
+ "matrix-sdk-crypto",
+ "matrix-sdk-store-encryption",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "matrix-sdk-sled"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ded5a703ad8a82b8edfde808228711315c8761a5fbf7ac2b98ab4951dadd066"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "dashmap",
+ "derive_builder",
+ "fs_extra",
+ "futures-core",
+ "futures-util",
+ "matrix-sdk-base",
+ "matrix-sdk-common",
+ "matrix-sdk-crypto",
+ "matrix-sdk-store-encryption",
+ "ruma",
+ "serde",
+ "serde_json",
+ "sled",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "matrix-sdk-store-encryption"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ddee75c3cca58f3a323283dc4e849d19d52988903f907ed0fb53dcad5d6fd25"
+dependencies = [
+ "blake3 1.4.0",
+ "chacha20poly1305 0.9.1",
+ "displaydoc",
+ "hmac 0.12.1",
+ "pbkdf2",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha2 0.10.8",
+ "thiserror",
+ "zeroize",
+]
+
+[[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 = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "memchr"
+version = "2.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[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 = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "minio-rsc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed652e35ba1eb4d333064e6f1c7ddf5855842dab17c5c6865edba5822382802"
+dependencies = [
+ "async-mutex",
+ "async-stream",
+ "base64 0.21.4",
+ "bytes",
+ "chrono",
+ "crc32fast",
+ "futures",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hmac 0.12.1",
+ "hyper",
+ "md5",
+ "once_cell",
+ "regex",
+ "reqwest",
+ "serde",
+ "sha2 0.10.8",
+ "urlencoding",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "multer"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http",
+ "httparse",
+ "log",
+ "memchr",
+ "mime",
+ "spin 0.9.8",
+ "version_check",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[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 1.1.0",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+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 = "openssl"
+version = "0.10.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd2523381e46256e40930512c7fd25562b9eae4812cb52078f155e87217c9d1e"
+dependencies = [
+ "bitflags",
+ "cfg-if 1.0.0",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "176be2629957c157240f68f61f2d0053ad3a4ecfdd9ebf1e6521d18d9635cf67"
+dependencies = [
+ "autocfg 1.1.0",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[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 0.9.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest 0.10.7",
+ "hmac 0.12.1",
+ "password-hash",
+ "sha2 0.10.8",
+]
+
+[[package]]
+name = "pem"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
+dependencies = [
+ "base64 0.13.1",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[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_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.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pipeline"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0"
+
+[[package]]
+name = "pkcs7"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7364e6d0e236473de91e042395d71e0e64715f99a60620b014a4a4c7d1619b"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "polling"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa"
+dependencies = [
+ "autocfg 1.1.0",
+ "bitflags",
+ "cfg-if 1.0.0",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite 0.2.13",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
+dependencies = [
+ "cpuid-bool",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "polyval"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[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 0.8.5",
+ "sha-1",
+ "sha2 0.9.9",
+]
+
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2 1.0.68",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc 0.1.0",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[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 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom 0.2.8",
+ "redox_syscall 0.2.16",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+
+[[package]]
+name = "reqwest"
+version = "0.11.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
+dependencies = [
+ "base64 0.21.4",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite 0.2.13",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin 0.5.2",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "route-recognizer"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
+
+[[package]]
+name = "ruma"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dc348e3a4a18abc4e97fffa5e2e623f6edd50ba3a1dd5f47eb249fea713b69f"
+dependencies = [
+ "assign",
+ "js_int",
+ "js_option",
+ "ruma-client-api",
+ "ruma-common",
+ "ruma-federation-api",
+]
+
+[[package]]
+name = "ruma-client-api"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1e72bc731b4dc8b569aa83915f13e419144b67110d858c65bb74aa05e2dc4b7"
+dependencies = [
+ "assign",
+ "bytes",
+ "http",
+ "js_int",
+ "maplit",
+ "percent-encoding",
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ruma-common"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "716889595f4edc3cfeb94d9f122e413f73e37d7d80ea1c14196e1004241a3889"
+dependencies = [
+ "base64 0.13.1",
+ "bytes",
+ "form_urlencoded",
+ "getrandom 0.2.8",
+ "http",
+ "indexmap 1.9.2",
+ "itoa",
+ "js-sys",
+ "js_int",
+ "js_option",
+ "percent-encoding",
+ "rand 0.8.5",
+ "regex",
+ "ruma-identifiers-validation",
+ "ruma-macros",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "url",
+ "uuid 1.4.1",
+ "wildmatch",
+]
+
+[[package]]
+name = "ruma-federation-api"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f905d12f6144c7a754bd0339fa6893698c03d03a908abb20cc6eeb4ec7f9466"
+dependencies = [
+ "js_int",
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ruma-identifiers-validation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebefdab34311af44d07cd2cd91c36cfe6a8c770647c6b00f6ab47f1186b2bb72"
+dependencies = [
+ "js_int",
+ "thiserror",
+]
+
+[[package]]
+name = "ruma-macros"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f82e91eb61cd86d9287303133ee55b54618eccb75a522cc22a42c15f5bda340"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "ruma-identifiers-validation",
+ "serde",
+ "syn 1.0.109",
+ "toml",
+]
+
+[[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.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.36.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[package]]
+name = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "scratch"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
+
+[[package]]
+name = "security-framework"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-this-or-that"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634c5a3cb041e56cc2964386151c67d520f845445789da3bd46bfb1c94f5e3bb"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c5113243e4a3a1c96587342d067f3e6b0f50790b6cf40d2868eb647a3eef0e"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[package]]
+name = "serde_fmt"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_qs"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
+dependencies = [
+ "percent-encoding",
+ "serde",
+ "thiserror",
+]
+
+[[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 1.0.0",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if 1.0.0",
+ "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 1.0.0",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[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 = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+
+[[package]]
+name = "simple-mutex"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6"
+dependencies = [
+ "event-listener",
+]
+
+[[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 0.3.20",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+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"
+
+[[package]]
+name = "spki"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27"
+dependencies = [
+ "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.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105"
+dependencies = [
+ "ahash",
+ "atoi",
+ "base64 0.13.1",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "dirs",
+ "dotenvy",
+ "either",
+ "event-listener",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "hkdf 0.12.3",
+ "hmac 0.12.1",
+ "indexmap 1.9.2",
+ "itoa",
+ "libc",
+ "log",
+ "md-5 0.10.5",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha1 0.10.5",
+ "sha2 0.10.8",
+ "smallvec",
+ "sqlformat",
+ "sqlx-rt",
+ "stringprep",
+ "thiserror",
+ "tokio-stream",
+ "url",
+ "uuid 1.4.1",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "once_cell",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "sha2 0.10.8",
+ "sqlx-core",
+ "sqlx-rt",
+ "syn 1.0.109",
+ "url",
+]
+
+[[package]]
+name = "sqlx-rt"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396"
+dependencies = [
+ "native-tls",
+ "once_cell",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "standback"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "stdweb"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "serde",
+ "serde_derive",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1 0.6.1",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+
+[[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 = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+
+[[package]]
+name = "strum_macros"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232"
+dependencies = [
+ "heck",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "rustversion",
+ "syn 2.0.38",
+]
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "sval"
+version = "1.0.0-alpha.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
+dependencies = [
+ "cfg-if 1.0.0",
+ "fastrand",
+ "redox_syscall 0.2.16",
+ "rustix",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tide"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0"
+dependencies = [
+ "async-h1",
+ "async-session",
+ "async-sse",
+ "async-std",
+ "async-trait",
+ "femme",
+ "futures-util",
+ "http-client",
+ "http-types",
+ "kv-log-macro",
+ "log",
+ "pin-project-lite 0.2.13",
+ "route-recognizer",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tide-compress"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000ffc53bf09b9307c3e333f2d411a501b29a980d189a8e3cc1186a78d673d14"
+dependencies = [
+ "async-compression",
+ "futures-lite",
+ "http-types",
+ "phf",
+ "regex",
+ "tide",
+]
+
+[[package]]
+name = "tide-csrf"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9051a9b530cb3c61bc56ab5d3fdf3b11db93777dd66c157d78672e1dc654cbe3"
+dependencies = [
+ "csrf",
+ "data-encoding",
+ "hkdf 0.11.0",
+ "serde_urlencoded",
+ "sha2 0.9.9",
+ "tide",
+]
+
+[[package]]
+name = "time"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
+dependencies = [
+ "const_fn",
+ "libc",
+ "standback",
+ "stdweb",
+ "time-macros 0.1.1",
+ "version_check",
+ "winapi",
+]
+
+[[package]]
+name = "time"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros 0.2.8",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
+dependencies = [
+ "proc-macro-hack",
+ "time-macros-impl",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "time-macros-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "standback",
+ "syn 1.0.109",
+]
+
+[[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.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.12.1",
+ "pin-project-lite 0.2.13",
+ "signal-hook-registry",
+ "socket2 0.5.4",
+ "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 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313"
+dependencies = [
+ "futures-core",
+ "pin-project-lite 0.2.13",
+ "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 0.2.13",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+
+[[package]]
+name = "toml_edit"
+version = "0.19.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
+dependencies = [
+ "indexmap 2.0.0",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "pin-project-lite 0.2.13",
+ "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 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]
+
+[[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 = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
+[[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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[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-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "universal-hash"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "uuid"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+dependencies = [
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "uuid"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
+dependencies = [
+ "getrandom 0.2.8",
+ "rand 0.8.5",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "value-bag"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42ee182c3667b7d22b228b3f5cf9a204559c892e52b0cb424d1275693eeb6f9"
+dependencies = [
+ "ctor",
+ "version_check",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.0.0-alpha.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
+dependencies = [
+ "ctor",
+ "erased-serde",
+ "serde",
+ "serde_fmt",
+ "sval",
+ "version_check",
+]
+
+[[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 = "vodozemac"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f20153a1c82ac5f1243b62e80f067ae608facc415c6ef82f88426a61c79886"
+dependencies = [
+ "aes 0.8.3",
+ "arrayvec 0.7.4",
+ "base64 0.13.1",
+ "cbc",
+ "ed25519-dalek",
+ "hkdf 0.12.3",
+ "hmac 0.12.1",
+ "pkcs7",
+ "prost",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "sha2 0.10.8",
+ "subtle",
+ "thiserror",
+ "x25519-dalek",
+ "zeroize",
+]
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[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.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if 1.0.0",
+ "serde",
+ "serde_json",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote 1.0.33",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 1.0.109",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "wasm-streams"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-timer"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
+dependencies = [
+ "futures",
+ "js-sys",
+ "parking_lot 0.11.2",
+ "pin-utils",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "whoami"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wildmatch"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86"
+
+[[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-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[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-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[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.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "winnow"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if 1.0.0",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "x25519-dalek"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.5.1",
+ "serde",
+ "zeroize",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2 1.0.68",
+ "quote 1.0.33",
+ "syn 2.0.38",
+]

+ 41 - 0
Cargo.toml

@@ -0,0 +1,41 @@
+[package]
+name = "kinbrio"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tide = {version="*", features=["cookies"]}
+askama = "*"
+tokio = { version = "*", features = ["full"] }
+async-std = { version = "*", features = ["tokio1"] }
+async-trait = "*"
+sqlx = { version="*",  features = [ "postgres", "runtime-tokio-native-tls", "uuid"] } 
+serde = {version= "*"}
+serde_derive = "*"
+value-bag="*"
+serde_json = {version= "*"}
+serde-this-or-that = "*"
+uuid = {version="*", features = [
+    "v4",                # Lets you generate random UUIDs
+    "fast-rng",          # Use a faster (but still sufficiently random) RNG
+    "serde",
+]}
+dotenv="*"
+tide-csrf="*"
+jsonwebtoken="*"
+pwhash="*"
+chrono ="*"
+matrix-sdk="*"
+anyhow="*"
+url="*"
+markdown="*"
+tide-compress="*"
+strum = "*"
+strum_macros = "*" 
+lazy_static="*"
+multer="*"
+reqwest="*"
+base64="*"
+minio-rsc="*"

+ 48 - 0
Dockerfile

@@ -0,0 +1,48 @@
+FROM rust:latest AS builder
+
+RUN update-ca-certificates
+
+# Create appuser
+ENV USER=kinbrio
+ENV UID=10001
+
+RUN adduser \
+    --disabled-password \
+    --gecos "" \
+    --home "/nonexistent" \
+    --shell "/sbin/nologin" \
+    --no-create-home \
+    --uid "${UID}" \
+    "${USER}"
+
+
+WORKDIR /kinbrio
+
+COPY ./ .
+
+# We no longer need to use the x86_64-unknown-linux-musl target
+RUN cargo build --release
+
+####################################################################################################
+## Final image
+####################################################################################################
+FROM debian:buster-slim
+
+# Import from builder.
+COPY --from=builder /etc/passwd /etc/passwd
+COPY --from=builder /etc/group /etc/group
+
+WORKDIR /kinbrio
+
+# Copy our build
+COPY --from=builder /kinbrio/target/release/kinbrio ./
+
+# Use an unprivileged user.
+USER kinbrio:kinbrio
+
+CMD ["/kinbrio/kinbrio"]
+RUN apt-get -y install build-essential
+RUN curl https://sh.rustup.rs -sSf | sh
+RUN apt-get -y install postgresql-14
+RUN cargo run
+EXPOSE 8080

文件差异内容过多而无法显示
+ 8 - 0
SPECS/muzli-colors.svg


文件差异内容过多而无法显示
+ 1 - 0
assets/css/calendar.js.min.css


文件差异内容过多而无法显示
+ 0 - 0
assets/css/mini-default.min.css


+ 533 - 0
assets/css/styles.css

@@ -0,0 +1,533 @@
+body {
+    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif;
+    line-height: 1.4;
+    margin-top: 20px; 
+    color: #1f2129;
+    background: #fdfdfd;
+    width:100%;
+}
+
+img {
+    border-radius: 5px 5px 0 0;
+}
+
+.card:hover {
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
+}
+
+.card_container {
+    padding: 2px 16px;
+}
+
+.card {
+    background-color:#EEFCFE;
+}
+
+.container .card {
+    padding: 10px;
+}
+
+#menu {
+    background: #EEFCFE;
+    color: #444; 
+    border: .0625rem solid var(#444);
+    margin:0;
+}
+nav a, nav a:visited {
+    color: black;
+    font-weight: bolder;
+}
+nav label {
+    color: black;
+    font-weight: bolder;
+}
+h4 {
+    text-align: left;
+}
+.delete_button {
+    background: #ff000087;
+    color: rgba(255, 253, 253, 0.904);
+    border-color: #FFDDC1;
+    border-radius: 8px;
+    cursor: pointer;
+    display: inline-block;
+    font-family: CerebriSans-Regular, -apple-system, system-ui, Roboto, sans-serif;
+
+    padding: 7px 20px;
+    text-align: center;
+    text-decoration: none;
+    border: 0;
+    font-size: 16px;
+    font-weight: bold;
+    user-select: none;
+    -webkit-user-select: none;
+    touch-action: manipulation;
+    margin: 4px;
+}
+ul.pick_list {
+    margin: 12px;
+    list-style-type: none;
+}
+
+ul.pick_list li a {
+    padding: 10px;
+    border: white solid 1px;
+    background-color: #e1e8ff;
+}
+
+input[type="text"] {
+    border: 1px solid gray;
+}
+
+.button {
+    display: inline-flex;
+    background: #FCFCD4;
+    margin: 4px;
+    border-radius: 5px;
+    color: black;
+    font-weight: bold;
+    border: 2px solid #0b8ae694;
+    padding: 0.5em;
+    text-decoration: none;
+}
+
+.button:focus,
+.button:hover {
+    background: #e1e8ffa4;
+    color: black;
+    font-weight: bold;
+}
+
+
+input[type="submit"].add_button {
+    background: #bbffd3;
+    color: #1f2129;
+    border-radius: 8px;
+    cursor: pointer;
+    display: inline-block;
+    font-weight: bold;
+    padding: 7px 20px;
+    text-align: center;
+    text-decoration: none;
+    border: 2px outset #fff;
+    font-size: 14px;
+    user-select: none;
+    -webkit-user-select: none;
+    touch-action: manipulation;
+    margin: 2px;
+}
+
+
+.col-12>input[type="submit"] {
+    background: #f0f4ff;
+    border-style: outset;
+    border-color: white;
+}
+
+.list_form {
+    background-color: #EEFCFE;
+    border: none;
+}
+
+form {
+    background-color: #bfc3cf8a;
+    border: 1px solid black;
+    max-width: 420px;
+}
+
+form input {
+    width: 100%;
+}
+
+form button[type=submit] {
+    display: block;
+    margin: 20px auto 0;
+    width: 150px;
+    height: 40px;
+    border-radius: 25px;
+    border: none;
+    color: #eee;
+    font-weight: 700;
+    box-shadow: 1px 4px 10px 1px #aaa;
+
+    background: #207cca;
+    background: -moz-linear-gradient(left, #207cca 0%, #9f58a3 100%);
+    background: -webkit-linear-gradient(left, #207cca 0%, #9f58a3 100%);
+    background: linear-gradient(to right, #207cca 0%, #9f58a3 100%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#207cca', endColorstr='#9f58a3', GradientType=1);
+}
+
+
+select {
+    appearance: none;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    background-color: white;
+    border: 1px solid black;
+    padding: 8px;
+    margin: 0;
+    width: 100%;
+    font-family: inherit;
+    font-size: inherit;
+    cursor: inherit;
+    line-height: inherit;
+
+    z-index: 1;
+
+    outline: none;
+}
+
+.select {
+    display: grid;
+    grid-template-areas: "select";
+    align-items: center;
+    position: relative;
+
+    min-width: 15ch;
+    max-width: 30ch;
+    border: 1px solid var(--select-border);
+    border-radius: 0.25em;
+    padding: 0.25em 0.5em;
+    font-size: 1.25rem;
+    cursor: pointer;
+    line-height: 1.1;
+
+    background: linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%);
+}
+
+select[multiple] {
+    padding-right: 0;
+    height: 7rem;
+}
+
+.select::after {
+    grid-area: select;
+    content: "";
+    justify-self: end;
+    width: 0.8em;
+    height: 0.5em;
+    background-color: var(--select-arrow);
+    clip-path: polygon(100% 0%, 0 0%, 50% 100%);
+}
+
+select:focus+.focus {
+    position: absolute;
+    top: -1px;
+    left: -1px;
+    right: -1px;
+    bottom: -1px;
+    border: 2px solid var(--select-focus);
+    border-radius: inherit;
+}
+
+.wrapper {
+    display: flex;
+}
+
+input[type="submit"] {
+    background-color: #e1e8ff;
+    border-style: outset;
+    color: black;
+    border-style: outset;
+    height: 50px;
+    font: bold 15px arial, sans-serif;
+    text-shadow: none;
+}
+
+.block {
+    background-color: #84a6adbd;
+    border: 2px solid grey;
+    box-shadow: grey 0px 0px 8px 0px;
+}
+
+.calendar {
+    background-color: rgba(245, 245, 245, 0.582);
+}
+
+.day {
+    background-color: rgb(255, 255, 255);
+    border: 2px dotted rgba(119, 138, 156, 0.726);
+}
+
+.emphasized {
+    font-size: 1.5rem;
+    -webkit-text-stroke-width: 0.01em;
+    -webkit-text-stroke-color: #EEFCFE;
+}
+
+a {
+    color: #5f62d9
+}
+
+a:link {
+    color: #5f62d9
+
+}
+
+.tabbed {
+    overflow-x: hidden;
+    /* so we could easily hide the radio inputs */
+    /* margin: 32px 0; */
+    /* padding-bottom: 16px; */
+    border-bottom: 1px solid #ccc;
+}
+
+.tabbed [type="radio"] {
+    /* hiding the inputs */
+    display: none;
+}
+
+.tabs {
+    display: flex;
+    align-items: stretch;
+    list-style: none;
+    padding: 0;
+    border-bottom: 1px solid #ccc;
+}
+
+@media screen and (max-width: 684px) {
+    .tabs {
+        flex-wrap: nowrap;
+        flex-direction: column;
+    }
+} 
+.tab>label {
+    display: block;
+    margin-bottom: -1px;
+    padding: 12px 15px;
+    border: 1px solid #ccc;
+    background: #eee;
+    color: #000000;
+    font-size: 1rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 1px;
+    cursor: pointer;
+}
+
+.tab:hover label {
+    border-top-color: #333;
+    color: #333;
+}
+
+.tab-content {
+    display: none;
+}
+
+/* As we cannot replace the numbers with variables or calls to element properties, the number of this selector parts is our tab count limit */
+.tabbed [type="radio"]:nth-of-type(1):checked~.tabs .tab:nth-of-type(1) label,
+.tabbed [type="radio"]:nth-of-type(2):checked~.tabs .tab:nth-of-type(2) label,
+.tabbed [type="radio"]:nth-of-type(3):checked~.tabs .tab:nth-of-type(3) label,
+.tabbed [type="radio"]:nth-of-type(4):checked~.tabs .tab:nth-of-type(4) label,
+.tabbed [type="radio"]:nth-of-type(5):checked~.tabs .tab:nth-of-type(5) label,
+.tabbed [type="radio"]:nth-of-type(6):checked~.tabs .tab:nth-of-type(6) label {
+    border-bottom-color: #fff;
+    border-top-color: #B721FF;
+    background: #fff;
+    color: #222;
+}
+
+.tabbed [type="radio"]:nth-of-type(1):checked~.tab-content:nth-of-type(1),
+.tabbed [type="radio"]:nth-of-type(2):checked~.tab-content:nth-of-type(2),
+.tabbed [type="radio"]:nth-of-type(3):checked~.tab-content:nth-of-type(3),
+.tabbed [type="radio"]:nth-of-type(4):checked~.tab-content:nth-of-type(4),
+.tabbed [type="radio"]:nth-of-type(5):checked~.tab-content:nth-of-type(5),
+.tabbed [type="radio"]:nth-of-type(6):checked~.tab-content:nth-of-type(6) {
+    display: block;
+} 
+
+a.button {
+    background: #FCFCD4;
+    border: 2px outset #0b8ae694;
+    padding: 8px;
+    color: black;
+}
+
+a.button:hover {
+    background: #46b2ff59;
+}
+
+.row>.card>p>b {
+    font-size: 1.3rem;
+}
+
+.row>.col-sm-12.col-md-6>.card>p>b {
+    font-size: 1.3rem;
+}
+
+.flex-center {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+h1 {
+    text-align: center;
+    text-transform: uppercase;
+    letter-spacing: 0.1em;
+}
+h1,h2,h3,h4 {
+    margin-bottom:0px;
+}
+
+.backed {
+    box-shadow: 0 4px 8px 0 rgba(255, 255, 255, 0.2);
+    border-radius: 5px;
+    margin: 8px;
+    padding: 8px;
+    border: 2px solid rgba(0, 0, 0, 0.123);
+    background-color: #EEFCFE;
+}
+
+.white-backed {
+    background-color: #ffffff;
+    margin: 6px;
+    border-radius:6px;
+}
+
+.heading_div {
+    background-color: #EEFCFE; 
+    text-align: center; 
+    padding: 0 3% 0 3%;
+}
+
+#content {
+    text-align: center;
+    height: 100%;
+    width: 95%;
+    padding: 0 10px;
+    margin-left: auto;
+    margin-right: auto;
+    -webkit-text-stroke-width: 0.01em;
+    -webkit-text-stroke-color: #9df3ff;
+}
+
+@media screen and (min-width: 1024px) {
+    #content {
+        width: 80%;
+    }
+}
+
+#add_note_form {
+    text-align: initial;
+}
+
+.justified {
+    justify-content: center;
+}
+
+a:visited {
+    color: #000000;
+}
+
+textarea {
+    width: 100%;
+}
+
+.gantt {
+    display: grid;
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+    background: repeating-linear-gradient(to right, #f2f2f2, #ddd 2px, #fff 2px, #fff 14.25%);
+}
+
+.gantt div {
+    padding: 10px;
+}
+
+.gantt .head {
+    text-align: center;
+    font-weight: 700;
+    color: #fff;
+    background: #103a99;
+}
+
+.dot {
+    padding: 5px;
+    display: inline-block;
+    color: black;
+    font-size: large;
+    font-weight: bold;
+}
+
+.bump {
+    border: 3px outset #EEFCFE;
+    border-radius: 6px;
+}
+
+
+input[type=radio]#assistant {
+    position: absolute;
+    top: -9999px;
+    left: -9999px;
+}
+
+input[type=radio]#minimize_assistant {
+    position: absolute;
+    top: -9999px;
+    left: -9999px;
+}
+label#minimize_assistant {
+    -webkit-appearance: push-button;
+    -moz-appearance: button;
+    appearance: button;
+    display: inline-block;
+    margin: 60px 0 10px 0;
+    cursor: pointer;
+}
+
+/* Toggled State */
+input[type=radio]#minimize_assistant:checked+.assistant-window {
+    display: block;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin-left: auto;
+    margin-right: auto;
+    width: 428px;
+    height: auto;
+}
+
+label#assistant {
+    -webkit-appearance: push-button;
+    -moz-appearance: button;
+    appearance: button;
+    display: inline-block;
+    margin: 60px 0 10px 0;
+    cursor: pointer;
+}
+
+/* Default State */
+.assistant-window {
+    display: none;
+    width: 2px;
+    height: 2px;
+    z-index: 9999;
+}
+
+/* Toggled State */
+input[type=radio]#assistant:checked+.assistant-window {
+    display: block;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin-left: auto;
+    margin-right: auto;
+    width: 428px;
+    height: 850px;
+}
+
+input[type=radio]#assistant:checked+.assistant-window>iframe {
+    display: block !important;
+    height: 800px;
+    width: 400px;
+    background-color: #88f0ffb0;
+}
+
+
+.checkbutton button {
+    display: none;
+}

+ 0 - 0
assets/css/styles.min.css


二进制
assets/favicon.ico


二进制
assets/favicon.png


+ 61 - 0
assets/images/akaunting-logo-horizontal.svg

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="1400px" height="400px" viewBox="0 0 1400 400" enable-background="new 0 0 1400 400" xml:space="preserve">
+<g>
+	<path fill="#6DA252" d="M137.25,256.894c-14.731-13.501-24.204-32.647-24.988-54.013H63.894
+		c0.812,36.041,16.816,68.329,41.861,90.717L137.25,256.894z"/>
+	<path fill="#6DA252" d="M189.359,74.5c-68.24,0-123.758,54.464-125.46,122.293h48.376c1.681-41.122,35.549-73.943,77.084-73.943
+		c42.609,0,77.15,34.542,77.15,77.15c0,28.662-15.631,53.676-38.833,66.978l22.762,42.681
+		c38.431-21.451,64.421-62.521,64.421-109.658C314.859,130.688,258.671,74.5,189.359,74.5"/>
+	<path fill="#6DA252" d="M222.28,269.792c-9.985,4.718-21.145,7.358-32.92,7.358c-17.899,0-34.375-6.095-47.464-16.323
+		l-31.506,36.717c21.566,17.481,49.045,27.956,78.97,27.956c20.003,0,38.912-4.682,55.693-13.005L222.28,269.792z"/>
+	<rect x="266.509" y="200" fill="#6DA252" width="48.35" height="125.5"/>
+</g>
+<g>
+	<path fill="#404041" d="M393.526,270.249l54.733-137.635h22.309l55.205,137.635H501.48l-13.896-35.26h-56.718l-13.612,35.26
+		H393.526z M437.767,215.8h42.161c-5.799-16.573-12.762-36.205-20.892-58.892C449.583,183.125,442.493,202.756,437.767,215.8z"/>
+	<path fill="#404041" d="M542.128,270.249V131.385h21.175v82.052l37.718-40.175h29.115l-47.36,46.509l50.573,50.479h-28.17
+		l-41.876-43.483v43.483H542.128z"/>
+	<path fill="#404041" d="M688.744,272.423c-7.563,0-14.402-1.403-20.514-4.205c-6.114-2.805-11.06-6.571-14.841-11.297
+		c-3.781-4.727-6.664-10.034-8.649-15.928c-1.985-5.892-2.978-12.084-2.978-18.576c0-6.807,1.055-13.296,3.166-19.473
+		c2.111-6.176,5.119-11.658,9.028-16.448c3.906-4.789,8.899-8.603,14.983-11.438c6.08-2.836,12.809-4.254,20.181-4.254
+		c6.429,0,12.162,1.371,17.205,4.112c5.041,2.741,9.074,6.413,12.099,11.013v-12.667h20.702v96.988h-21.079v-14.557
+		c-2.9,5.042-6.888,9.091-11.958,12.146C701.017,270.896,695.233,272.423,688.744,272.423z M691.675,255.219
+		c9.263,0,16.163-3.072,20.701-9.217c4.537-6.145,6.806-14.353,6.806-24.624c0-10.082-2.349-18.181-7.042-24.296
+		c-4.695-6.11-11.611-9.168-20.75-9.168c-9.391,0-16.48,3.072-21.268,9.217c-4.791,6.145-7.186,14.29-7.186,24.436
+		c0,9.959,2.411,18.056,7.232,24.294S682.158,255.219,691.675,255.219z"/>
+	<path fill="#404041" d="M804.164,272.423c-12.163,0-21.443-3.481-27.839-10.445c-6.397-6.961-9.595-16.243-9.595-27.839v-60.877
+		h20.985v60.689c0,7.122,1.78,12.446,5.342,15.975c3.56,3.532,8.615,5.294,15.172,5.294c7.562,0,13.296-2.016,17.203-6.05
+		c3.906-4.032,5.862-10.021,5.862-17.961v-57.947h21.174v71.655c0,8.067,0.315,16.512,0.945,25.333h-21.741l-0.378-14.746
+		c-0.757,1.766-1.796,3.53-3.12,5.293c-1.323,1.766-3.059,3.579-5.198,5.435c-2.145,1.86-4.855,3.357-8.131,4.492
+		C811.568,271.856,808.007,272.423,804.164,272.423z"/>
+	<path fill="#404041" d="M883.002,270.249v-63.996c0-5.86-0.128-11.532-0.378-17.016c-0.253-5.483-0.506-9.516-0.756-12.1
+		l-0.379-3.876h22.216v13.708c3.338-5.167,7.876-9.153,13.611-11.959c5.733-2.802,11.5-4.206,17.299-4.206
+		c11.973,0,20.953,3.592,26.941,10.776c5.986,7.185,8.98,18.622,8.98,34.315v54.354h-21.175v-55.016
+		c0-9.579-1.403-16.527-4.206-20.845c-2.806-4.315-7.737-6.475-14.794-6.475c-6.114,0-12.006,2.348-17.678,7.043
+		s-8.508,10.067-8.508,16.116v59.176H883.002z"/>
+	<path fill="#404041" d="M1053.346,270.154h-9.075c-6.934,0-12.636-0.439-17.11-1.322c-4.476-0.882-8.446-2.554-11.91-5.011
+		c-3.467-2.458-5.972-5.986-7.516-10.587c-1.545-4.6-2.315-10.524-2.315-17.771v-48.022h-19.285v-14.18h19.853v-23.632l20.513-6.144
+		v29.776h26.468v14.18h-26.468v49.25c0,3.971,0.345,7.043,1.04,9.217c0.692,2.174,2.14,3.894,4.348,5.153
+		c2.206,1.262,4.853,2.049,7.941,2.363c3.086,0.315,7.593,0.472,13.518,0.472V270.154z"/>
+	<path fill="#404041" d="M1076.316,270.249v-96.988h20.702v96.988H1076.316z M1076.411,151.52v-18.906h20.607v18.906H1076.411z"/>
+	<path fill="#404041" d="M1126.89,270.249v-63.996c0-5.86-0.128-11.532-0.378-17.016c-0.253-5.483-0.505-9.516-0.757-12.1
+		l-0.378-3.876h22.216v13.708c3.339-5.167,7.877-9.153,13.612-11.959c5.732-2.802,11.499-4.206,17.299-4.206
+		c11.972,0,20.952,3.592,26.94,10.776c5.986,7.185,8.98,18.622,8.98,34.315v54.354h-21.175v-55.016
+		c0-9.579-1.403-16.527-4.207-20.845c-2.805-4.315-7.736-6.475-14.794-6.475c-6.114,0-12.005,2.348-17.677,7.043
+		c-5.671,4.695-8.508,10.067-8.508,16.116v59.176H1126.89z"/>
+	<path fill="#404041" d="M1282.298,309.385c-12.92,0-25.146-2.586-36.678-7.752l3.497-18.339
+		c10.271,5.923,20.952,8.886,32.046,8.886c9.514,0,16.903-2.302,22.168-6.9c5.259-4.601,7.893-11.471,7.893-20.607v-9.169
+		c-6.178,9.074-15.158,13.611-26.941,13.611c-13.801,0-25.019-4.283-33.653-12.855c-8.636-8.568-12.95-20.135-12.95-34.692
+		c0-7.121,0.96-13.721,2.884-19.804c1.921-6.081,4.727-11.438,8.412-16.071c3.688-4.632,8.462-8.271,14.322-10.917
+		c5.86-2.647,12.509-3.971,19.945-3.971c6.618,0,12.272,1.34,16.97,4.018c4.693,2.679,8.396,6.129,11.105,10.351v-11.911h22.215
+		c-1.009,9.831-1.512,19.852-1.512,30.06v53.032c0,5.546-0.442,10.729-1.323,15.55c-0.885,4.821-2.476,9.596-4.773,14.322
+		c-2.303,4.727-5.28,8.744-8.934,12.052c-3.656,3.309-8.429,5.986-14.321,8.035C1296.774,308.359,1289.984,309.385,1282.298,309.385
+		z M1285.7,252.949c4.663,0,8.744-0.864,12.242-2.599c3.497-1.732,6.254-4.143,8.271-7.231c2.016-3.086,3.497-6.522,4.441-10.304
+		c0.946-3.781,1.419-7.941,1.419-12.478c0-9.769-2.207-17.614-6.617-23.539c-4.413-5.922-10.714-8.885-18.905-8.885
+		c-8.509,0-15.096,3.136-19.757,9.406c-4.665,6.272-6.995,14.291-6.995,24.059c0,10.02,2.188,17.787,6.569,23.301
+		C1270.748,250.195,1277.192,252.949,1285.7,252.949z"/>
+</g>
+</svg>

二进制
assets/images/bot_dark.webp


二进制
assets/images/logo_transparent.png


文件差异内容过多而无法显示
+ 8 - 0
assets/images/matrix-logo-white.svg


二进制
assets/images/matrix.png


+ 22 - 0
assets/images/sso/apple.svg

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 viewBox="0 0 52.226 52.226" xml:space="preserve">
+<g>
+	<g>
+		<path d="M36.802,1.055L36.747,0l-1.05,0.113c-0.103,0.011-10.252,1.234-10.948,12.581l-0.07,1.136l1.136-0.077
+			C25.931,13.745,37.426,12.828,36.802,1.055z M34.821,2.322c-0.191,6.838-5.511,8.74-7.953,9.253
+			C27.798,4.93,32.617,2.905,34.821,2.322z"/>
+		<path d="M46.584,37.517l-0.639-0.207c-3.867-1.25-6.464-4.792-6.464-8.814c0-3.578,2.023-6.778,5.281-8.352l0.972-0.469
+			l-0.542-0.933c-0.232-0.4-2.401-3.943-6.983-5.116c-3.634-0.93-7.72-0.107-12.149,2.447c-1.875-1.138-8.103-4.418-13.058-1.13
+			c-0.97,0.536-11.251,6.695-5.9,23.313c0.157,0.372,3.888,9.113,8.303,12.387c1.191,1.138,4.237,2.56,7.718,0.187
+			c0.603-0.249,4.638-1.802,7.198,0.017c0.945,0.647,2.595,1.38,4.338,1.38c1.322,0,2.697-0.421,3.859-1.621
+			c0.542-0.469,5.493-4.888,8.066-11.888l0.075-0.204L46.584,37.517z M37.182,49.115l-0.077,0.073
+			c-2.193,2.303-5.518,0.1-5.641,0.018c-1.308-0.93-2.823-1.233-4.244-1.233c-2.579,0-4.847,0.999-4.992,1.064l-0.163,0.092
+			c-3.019,2.107-5.086,0.253-5.305,0.042l-0.118-0.101c-3.993-2.912-7.663-11.507-7.668-11.51
+			C3.966,21.992,13.56,16.9,13.968,16.693l0.11-0.065c4.647-3.12,11.327,1.396,11.393,1.441l0.533,0.366l0.552-0.333
+			c4.16-2.515,7.914-3.37,11.157-2.539c2.642,0.676,4.326,2.327,5.15,3.342c-3.347,2.051-5.381,5.63-5.381,9.591
+			c0,4.556,2.735,8.604,6.902,10.365C41.819,45.122,37.231,49.074,37.182,49.115z"/>
+	</g>
+</g>
+</svg>

二进制
assets/images/sso/beta-jesse-lee-peterson.gif


+ 12 - 0
assets/images/sso/facebook.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
+	 viewBox="0 0 310 310" xml:space="preserve">
+<g id="XMLID_834_">
+	<path id="XMLID_835_" d="M81.703,165.106h33.981V305c0,2.762,2.238,5,5,5h57.616c2.762,0,5-2.238,5-5V165.765h39.064
+		c2.54,0,4.677-1.906,4.967-4.429l5.933-51.502c0.163-1.417-0.286-2.836-1.234-3.899c-0.949-1.064-2.307-1.673-3.732-1.673h-44.996
+		V71.978c0-9.732,5.24-14.667,15.576-14.667c1.473,0,29.42,0,29.42,0c2.762,0,5-2.239,5-5V5.037c0-2.762-2.238-5-5-5h-40.545
+		C187.467,0.023,186.832,0,185.896,0c-7.035,0-31.488,1.381-50.804,19.151c-21.402,19.692-18.427,43.27-17.716,47.358v37.752H81.703
+		c-2.762,0-5,2.238-5,5v50.844C76.703,162.867,78.941,165.106,81.703,165.106z"/>
+</g>
+</svg>

+ 1 - 0
assets/images/sso/github.svg

@@ -0,0 +1 @@
+<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

+ 23 - 0
assets/images/sso/gitlab.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 26.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 100 92" style="enable-background:new 0 0 100 92;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#E24329;}
+	.st1{fill:#FC6D26;}
+	.st2{fill:#FCA326;}
+</style>
+<desc>Created with Sketch.</desc>
+<g>
+	<path class="st0" d="M95.9,36.5l-0.1-0.3L82.8,2.4c-0.3-0.7-0.7-1.2-1.3-1.6c-0.6-0.4-1.3-0.6-2-0.5c-0.7,0-1.4,0.3-2,0.7
+		c-0.6,0.4-1,1.1-1.1,1.7l-8.7,26.7H32.3L23.6,2.7C23.4,2.1,23,1.4,22.5,1c-0.6-0.4-1.2-0.7-2-0.7c-0.7,0-1.4,0.1-2,0.5
+		c-0.6,0.4-1.1,0.9-1.3,1.6L4.2,36.1l-0.1,0.3c-3.8,10-0.6,21.3,8,27.7c0,0,0,0,0,0l0.1,0.1l19.7,14.7l9.7,7.4l5.9,4.5
+		c1.4,1.1,3.4,1.1,4.8,0l5.9-4.5l9.7-7.4l19.8-14.8c0,0,0,0,0.1,0C96.5,57.8,99.7,46.5,95.9,36.5z"/>
+	<path class="st1" d="M95.9,36.5l-0.1-0.3c-6.4,1.3-12.3,4-17.4,7.8C78.3,44,63,55.6,50,65.4c9.7,7.3,18.1,13.7,18.1,13.7l19.8-14.8
+		c0,0,0,0,0.1,0C96.5,57.8,99.7,46.5,95.9,36.5z"/>
+	<path class="st2" d="M31.9,79.1l9.7,7.4l5.9,4.5c1.4,1.1,3.4,1.1,4.8,0l5.9-4.5l9.7-7.4c0,0-8.4-6.4-18.1-13.7
+		C40.3,72.7,31.9,79.1,31.9,79.1z"/>
+	<path class="st1" d="M21.6,43.9c-5.1-3.8-11-6.5-17.4-7.8l-0.1,0.3c-3.8,10-0.6,21.3,8,27.7c0,0,0,0,0,0l0.1,0.1l19.7,14.7
+		c0,0,8.4-6.4,18.1-13.7C37,55.6,21.7,44,21.6,43.9z"/>
+</g>
+</svg>

+ 5 - 0
assets/images/sso/google.svg

@@ -0,0 +1,5 @@
+<?xml version="1.0" standalone="no"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" class="icon">
+  <path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm167 633.6C638.4 735 583 757 516.9 757c-95.7 0-178.5-54.9-218.8-134.9C281.5 589 272 551.6 272 512s9.5-77 26.1-110.1c40.3-80.1 123.1-135 218.8-135 66 0 121.4 24.3 163.9 63.8L610.6 401c-25.4-24.3-57.7-36.6-93.6-36.6-63.8 0-117.8 43.1-137.1 101-4.9 14.7-7.7 30.4-7.7 46.6s2.8 31.9 7.7 46.6c19.3 57.9 73.3 101 137 101 33 0 61-8.7 82.9-23.4 26-17.4 43.2-43.3 48.9-74H516.9v-94.8h230.7c2.9 16.1 4.4 32.8 4.4 50.1 0 74.7-26.7 137.4-73 180.1z"/>
+</svg>

二进制
assets/images/sso/user_password.svg


+ 74 - 0
assets/js/app.js

@@ -0,0 +1,74 @@
+function num_from_string(inp) {
+    let n = parseInt(inp)
+    if (isNaN(n)) {
+        return 0
+    }
+    return n
+}
+function set_value(element, val) {
+    if (element) {
+        element.value = val;
+    }
+}
+const dateForDateTimeInputValue = date => new Date(date.getTime() + new Date().getTimezoneOffset() * -60 * 1000).toISOString().slice(0, 19)
+
+function send_delete(button_id, url, cb) {
+    cb = cb || (() => { });
+    var btn = document.getElementById(button_id);
+    btn.onclick = function (event) {
+        let confirmed = confirm(("Delete!?"))
+        if (!confirmed) {
+            return cb(false)
+        }
+        event.preventDefault();
+        var xhr = new XMLHttpRequest(); 
+        xhr.open('DELETE', url)
+        xhr.onreadystatechange = function () {
+            if (xhr.readyState == XMLHttpRequest.DONE) {
+                cb(true, xhr.responseText)
+            }
+        }
+        xhr.send(null)
+        return false;
+    }
+}
+
+function post_form(form_id, url, validation, cb) {
+    validation = validation || (() => true);
+    cb = cb || (() => { });
+
+    var form = document.getElementById(form_id);
+    form.onsubmit = function (event) {
+        event.preventDefault();
+        var xhr = new XMLHttpRequest();
+        var formData = new FormData(form);
+        formData = validation(Object.fromEntries(formData));
+        if (!formData) {
+            return
+        }
+        xhr.open('POST', url)
+        xhr.setRequestHeader("Content-Type", form.enctype||"application/json");
+        xhr.send(JSON.stringify(formData));
+        xhr.onreadystatechange = function () {
+            if (xhr.readyState == XMLHttpRequest.DONE) {
+                cb(xhr.responseText)
+            }
+        }
+        return false;
+    }
+}
+
+function deleteAllCookies() {
+    var cookies = document.cookie.split(";");
+    for (var i = 0; i < cookies.length; i++) {
+        var cookie = cookies[i];
+        var eqPos = cookie.indexOf("=");
+        var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
+        document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+    }
+}
+
+function clearAndRedirect(link) {
+    deleteAllCookies();
+    document.location = link;
+}

+ 222 - 0
assets/js/calendar.min.js

@@ -0,0 +1,222 @@
+/*! Calendar.js v2.0.12 | (c) Bunoon | GNU AGPLv3 License */
+function calendarJs(Hi,Ii,Ji){function Z(a,b,d){A=ud(a)?a:new Date;A.setDate(1);A.setHours(0,0,0,0);a=A;var e=new Date;ah=a.getFullYear()===e.getFullYear()&&a.getMonth()===e.getMonth();b=r(b)?b:!1;d=r(d)?d:!1;a=new Date(A.getFullYear(),A.getMonth(),1);a=0===a.getDay()?7:a.getDay();e=vd(A.getFullYear(),A.getMonth());wd()&&xd();da();if(!db){if(null===G){if(bh(ia)){var f=ia;ia=f.id;ja(ia)||(ia=ch())}else f=tb(ia);if(null!==f)if("input"!==f.tagName.toLowerCase()||"text"!==f.type&&"date"!==f.type)G=f,
+G.className="calendar",G.innerHTML=n.empty;else{var g=f;yd=g;ub(yd,"hidden");cc=k("input","calendar-date-picker-input");cc.readOnly=!0;cc.placeholder=c.selectDatePlaceholderText;t=!0;f=g.parentNode;f.removeChild(g);g=k("div","calendar-date-picker");f.appendChild(g);g.appendChild(yd);g.appendChild(cc);G=k("div","calendar calendar-hidden");G.id=ia;g.appendChild(G);cc.onclick=Ki;cc.onkeydown=Li;B.addEventListener("click",Ec);dh();g=yd.value.split("/");f=null;3===g.length&&(g=new Date(parseInt(g[2]),
+parseInt(g[1])-1,parseInt(g[0])),g instanceof Date&&!isNaN(g)&&(f=g));null===f?f=new Date:Vf(f);f.setHours(0,0,0,0);Ib=f}}if(null!==G){t||null!==Ra||(zd=k("div","side-menu-disabled-background"),zd.onclick=xd,G.appendChild(zd),Mi());Ni();Oi();Pi();Qi();g=null!==Jb;if(c.showDayNamesInMainDisplay)for(f=c.dayHeaderNames.length,g&&(Jb.innerHTML=n.empty),g||(Jb=k("div","row-cells header-days"),G.appendChild(Jb)),t&&(Jb.onclick=D),g=0;g<f;g++)-1<c.visibleDays.indexOf(g)&&Ri(Jb,g);else g&&(G.removeChild(Jb),
+Jb=null);if(0<Ad.length){f=Ad.length;for(g=0;g<f;g++)G.removeChild(Ad[g]);Ad=[]}for(f=0;6>f;f++){g=k("div","row-cells days");G.appendChild(g);Ad.push(g);for(var h=0;7>h;h++)if(-1<c.visibleDays.indexOf(h)){var l=7*f+(h+1),p=k("div",eh(c.allowEventScrollingOnMainDisplay));p.id="calendar-day-"+l;g.appendChild(p);c.allowEventScrollingOnMainDisplay&&(p.className+=" scrollY");0<c.minimumDayHeight&&(p.style.height=c.minimumDayHeight+"px")}}Wf||(fh(),Wf=!0);db=!0;za(c.events)&&qa.addEvents(c.events,!1,!1,
+!1);gh||(z("onRender",ia),gh=!0)}}if(1<a)for(f=new Date(A),f.setMonth(f.getMonth()-1),g=vd(f.getFullYear(),f.getMonth()),h=1,l=g-a+1;l<g;l++)p=l===g-1,Xf(l+1,h,f.getMonth(),f.getFullYear(),!0,p),h++;for(g=f=0;g<e;g++)f=a+g,Xf(g+1,f,A.getMonth(),A.getFullYear(),!1);if(42>f){e=1;a=new Date(A);a.setMonth(a.getMonth()+1);for(f+=1;43>f;f++)g=1===e,Xf(e,f,a.getMonth(),a.getFullYear(),!0,g),e++;e=vd(a.getFullYear(),a.getMonth());e=Math.round(e/2);Yf=new Date(a.getFullYear(),a.getMonth(),e)}else Yf=null;
+ra();b&&(null!==Aa||t||(Aa=k("div","disabled-background")),Si(),t||null!==Kb||(Kb=k("div","calendar-dialog event-editor-colors"),B.body.appendChild(Kb),b=k("div","title-bar"),y(b,c.selectColorsText),Kb.appendChild(b),Fc(b,Kb,null),w(b,"ib-close",c.closeTooltipText,ye,!0),b=k("div","contents"),Kb.appendChild(b),ka(b,c.backgroundColorText),Gc=k("input"),b.appendChild(Gc),ub(Gc,"color"),ka(b,c.textColorText),Hc=k("input"),b.appendChild(Hc),ub(Hc,"color"),ka(b,c.borderColorText),Ic=k("input"),b.appendChild(Ic),
+ub(Ic,"color"),a=k("div","buttons-container"),b.appendChild(a),sa(a,c.updateText,"update",Ti),sa(a,c.cancelText,"cancel",ye)),t||null!==Lb||(Lb=k("div","calendar-dialog event-editor-repeat-options"),B.body.appendChild(Lb),b=k("div","title-bar"),y(b,c.repeatOptionsTitle),Lb.appendChild(b),Fc(b,Lb,null),w(b,"ib-close",c.closeTooltipText,ze,!0),b=k("div","contents"),Lb.appendChild(b),ka(b,c.daysToExcludeText),Ae=M(b,c.dayNames[0])[0],Be=M(b,c.dayNames[1])[0],Ce=M(b,c.dayNames[2])[0],De=M(b,c.dayNames[3])[0],
+Ee=M(b,c.dayNames[4])[0],Fe=M(b,c.dayNames[5])[0],Ge=M(b,c.dayNames[6])[0],ka(b,c.repeatEndsText),dc=k("input"),b.appendChild(dc),ub(dc,"date"),a=k("div","buttons-container"),b.appendChild(a),sa(a,c.updateText,"update",Ui),sa(a,c.cancelText,"cancel",ze)),t||null!==ec||(ec=k("div","calendar-dialog message"),B.body.appendChild(ec),Zf=k("div","title-bar"),ec.appendChild(Zf),b=k("div","contents"),ec.appendChild(b),$f=k("div","text"),b.appendChild($f),a=M(b,c.removeAllEventsInSeriesText),He=a[0],ag=a[1],
+a=k("div","buttons-container"),b.appendChild(a),fc=k("input","yes-ok","button"),fc.value=c.yesText,a.appendChild(fc),gc=k("input","no","button"),gc.value=c.noText,a.appendChild(gc)),t||null!==Mb||(Mb=k("div","calendar-dialog export-events"),B.body.appendChild(Mb),b=k("div","title-bar"),y(b,c.exportEventsTitle),Mb.appendChild(b),Fc(b,Mb,null),w(b,"ib-close",c.closeTooltipText,Ie,!0),b=k("div","contents"),Mb.appendChild(b),Fa=k("input",null,"text"),Fa.placeholder=c.exportFilenamePlaceholderText,b.appendChild(Fa),
+a=k("div","split"),b.appendChild(a),e=k("div","radio-buttons-container split-contents"),a.appendChild(e),f=k("div","radio-buttons-container split-contents"),a.appendChild(f),bg=S(e,"CSV","ExportType"),hh=S(e,"XML","ExportType"),ih=S(e,"JSON","ExportType"),jh=S(e,"TEXT","ExportType"),kh=S(f,"iCAL","ExportType"),lh=S(f,"MD","ExportType"),mh=S(f,"HTML","ExportType"),nh=S(f,"TSV","ExportType"),a=k("div","buttons-container"),b.appendChild(a),sa(a,c.exportText,"export",Vi),sa(a,c.cancelText,"cancel",Ie)),
+Wi(),Xi(),Yi(),t||(Zi(),$i(),aj(),bj()));d&&ta(!0,!1);null!==G&&(cg.innerText=oh(c.monthTitleBarDateFormat,A))}function Jc(){var a=[];Sa(function(b){a.push(b)});return a}function Sa(a){for(var b in Ja)if(Ja.hasOwnProperty(b))for(var d in Ja[b])if(Ja[b].hasOwnProperty(d)){var e=ph(Ja[b][d]);if(a(e,b,d))return}}function Nb(a,b){b=r(b)?b:!0;a=a.sort(function(d,e){return d.from-e.from});b&&(a=a.sort(function(d,e){return qh(e.isAllDay)-qh(d.isAllDay)}));return a}function dg(a,b){vb(B.body,Aa);var d=function(){Ka(B.body,
+Aa)};Je(c.confirmEventsRemoveTitle,c.confirmEventsRemoveMessage,function(){d();Sa(function(e){I(e.repeatEvery)===x.never&&b(e.from,a)&&qa.removeEvent(e.id,!1)});ta()},d)}function Qi(){Ke=Bd=eg=null;var a=null!==V;a&&(V.innerHTML=n.empty);a||(V=k("div","header-date"),G.appendChild(V));c.fullScreenModeEnabled&&(V.ondblclick=eb);t&&(V.onclick=function(b){D(b);da()});t||(w(V,"ib-hamburger",c.showMenuTooltipText,Cd),a=k("div","side-menu-button-divider-line"),V.appendChild(a));w(V,"ib-arrow-left-full",
+c.previousMonthTooltipText,Dd);t&&c.addYearButtonsInDatePickerMode&&w(V,"ib-rewind",c.previousYearTooltipText,Le);(t||c.showExtraToolbarButtons)&&w(V,"ib-pin",c.currentMonthTooltipText,Me);c.showExtraToolbarButtons&&(w(V,"ib-refresh",c.refreshTooltipText,function(){ta(!0,!0)}),u.enabled&&(Ke=w(V,"ib-search",c.searchTooltipText,Ed)));w(V,"ib-arrow-right-full",c.nextMonthTooltipText,Fd);t&&c.addYearButtonsInDatePickerMode&&w(V,"ib-forward",c.nextYearTooltipText,Ne);t&&w(V,"ib-close",c.closeTooltipText,
+Ec);c.showExtraToolbarButtons&&(c.manualEditingEnabled&&w(V,"ib-plus",c.addEventTooltipText,Kc),c.exportEventsEnabled&&(eg=w(V,"ib-arrow-down-full-line",c.exportEventsTooltipText,function(){Lc(Mc)})));t||(w(V,"ib-eye",c.listAllEventsTooltipText,function(){rh(!0)}),w(V,"ib-hamburger-side",c.listWeekEventsTooltipText,function(){Nc(null,!0)}));c.showExtraToolbarButtons&&c.fullScreenModeEnabled&&(Bd=w(V,"ib-arrow-expand-left-right",c.enableFullScreenTooltipText,eb));a=k("div","title-container");V.appendChild(a);
+cj(a);dj(a)}function Ri(a,b){var d=c.dayHeaderNames[b],e=k("div",eh());y(e,d);a.appendChild(e);e.oncontextmenu=function(f){sh(f,b)};e.ondblclick=function(f){if(!t){var g=!1;if(0===hc.length){if(f=c.visibleDays.length,1<f){for(g=0;g<f;g++)hc.push(c.visibleDays[g]);c.visibleDays=[];c.visibleDays.push(b);g=!0}}else{c.visibleDays=[];f=hc.length;for(g=0;g<f;g++)c.visibleDays.push(hc[g]);hc=[];g=!0}g&&(db=!1,z("onOptionsUpdated",c),Z(A,!0))}}}function eh(a){a=r(a)?a:!1;var b="cell cell-"+c.visibleDays.length;
+a&&(b+=" custom-scroll-bars");return b}function ph(a){a.isAllDay&&(a.from=new Date(a.from.getFullYear(),a.from.getMonth(),a.from.getDate(),0,0),a.to=new Date(a.from.getFullYear(),a.from.getMonth(),a.from.getDate(),23,59));return a}function eb(){ic?th():uh()}function uh(){!ic&&c.fullScreenModeEnabled&&(vh(),z("onFullScreenModeChanged",!0))}function th(){ic&&c.fullScreenModeEnabled&&(ic=!1,G.className=G.className.replace(" full-screen-view",n.empty),G.style.cssText=wh,xh("ib-arrow-expand-left-right",
+c.enableFullScreenTooltipText),la(),z("onFullScreenModeChanged",!1))}function vh(){wh=G.style.cssText;ic=!0;G.className+=" full-screen-view";G.removeAttribute("style");xh("ib-arrow-contract-left-right",c.disableFullScreenTooltipText);la()}function xh(a,b){null!==Bd&&(Bd.className=a);null!==Oe&&(Oe.className=a);null!==Pe&&(Pe.className=a);null!==Qe&&(Qe.className=a);ua(Bd,b);ua(Oe,b);ua(Pe,b);ua(Qe,b)}function Mi(){Ra=k("div","side-menu custom-scroll-bars");G.appendChild(Ra);var a=k("div","main-header");
+Ra.appendChild(a);ka(a,c.sideMenuHeaderText);w(a,"ib-close",c.closeTooltipText,xd);c.configurationDialogEnabled&&w(a,"ib-octagon-hollow",c.configurationTooltipText,function(){xd();vb(B.body,Aa);fg.checked=Gd;gg.checked=c.eventNotificationsEnabled;hg.checked=c.tooltipsEnabled;ig.checked=c.dragAndDropForEventsEnabled;jg.checked=c.showDayNamesInMainDisplay;kg.checked=c.showEmptyDaysInWeekView;lg.checked=c.showHolidays;Re.value=c.organizerName;Se.value=c.organizerEmailAddress;La.push(mg);fb.style.display=
+"block"});Oc=k("div","content");Ra.appendChild(Oc)}function Cd(){yh();ng=!1;Ra.className+=" side-menu-open";zd.style.display="block"}function yh(){var a=og(Hd),b=og(Pc,!0),d=og(Qc,!0);Oc.innerHTML=n.empty;Id();a=r(a)?a:!0;Rc=k("div","content-section content-section-opened");Hd=k("div","checkbox-container");Oc.appendChild(Rc);pg(Rc,Hd,c.sideMenuDaysText,a);Rc.appendChild(Hd);for(a=0;7>a;a++){var e=-1<c.visibleDays.indexOf(a);M(Hd,c.dayNames[a],qg,a.toString(),e)}b=r(b)?b:!0;Pc=jc=null;a=!1;for(var f in K)K.hasOwnProperty(f)&&
+(a||(jc=k("div","content-section"),Oc.appendChild(jc),Pc=k("div","checkbox-container"),pg(jc,Pc,c.eventTypesText,b),jc.appendChild(Pc),a=!0),e=!0,r(L.visibleEventTypes)&&(e=-1<L.visibleEventTypes.indexOf(parseInt(f))),M(Pc,K[f].text,qg,f,e));b=d;b=r(b)?b:!0;Qc=kc=null;d=zh();f=d.length;if(0<f)for(kc=k("div","content-section"),Qc=k("div","checkbox-container"),Oc.appendChild(kc),pg(kc,Qc,c.groupsText,b),kc.appendChild(Qc),b=0;b<f;b++){a=d[b];e=a.toLowerCase();var g=!0;r(L.visibleGroups)&&(g=-1<L.visibleGroups.indexOf(e));
+M(Qc,a,qg,e,g)}}function wb(){wd()&&yh()}function xd(){if(null!==Ra&&(Ra.className="side-menu custom-scroll-bars",zd.style.display="none",ng)){null!==kc&&(L.visibleGroups=rg(kc),z("onVisibleGroupsChanged",L.visibleGroups));null!==jc&&(L.visibleEventTypes=rg(jc,!0),z("onVisibleEventTypesChanged",L.visibleEventTypes));if(null!==Rc){var a=rg(Rc,!0);1<=a.length&&(c.visibleDays=a,hc=[],z("onOptionsUpdated",c))}db=!1;Z(A,!0,!0)}}function wd(){return null!==Ra&&-1<Ra.className.indexOf("side-menu-open")}
+function rg(a,b){b=r(b)?b:!1;var d=a.getElementsByTagName("input"),e=d.length,f=[];if(0<e)for(var g=0;g<e;g++){var h=d[g];h.checked&&(b?f.push(parseInt(h.name)):f.push(h.name))}return f}function pg(a,b,d,e){var f=k("div","text-header");a.appendChild(f);y(f,d+":");var g=k("div","ib-arrow-up-full");f.appendChild(g);f.onclick=function(){var h="none"===b.style.display;f.className=h?"text-header":"text-header-closed";b.style.display=h?"block":"none";g.className=h?"ib-arrow-up-full":"ib-arrow-down-full";
+a.className=h?"content-section content-section-opened":"content-section"};e||(b.style.display="none",f.className="text-header-closed",g.className="ib-arrow-down-full",a.className="content-section")}function og(a,b){return b&&null===a?!1:null===a||"none"!==a.style.display}function qg(){ng=!0}function Ah(a){a=r(a)&&K.hasOwnProperty(a)?a:0;for(var b in K)K.hasOwnProperty(b)&&r(K[b].eventEditorInput)&&(K[b].eventEditorInput.checked=!1);r(K[a].eventEditorInput)&&(K[a].eventEditorInput.checked=!0)}function dh(){t&&
+(c.exportEventsEnabled=!1,c.manualEditingEnabled=!1,c.fullScreenModeEnabled=!1,c.eventNotificationsEnabled=!1,c.showPreviousNextMonthNamesInMainDisplay=!1,c.showExtraToolbarButtons=!1,c.holidays=[])}function Ki(a){D(a);a=B.getElementsByClassName("calendar calendar-shown");a=[].slice.call(a);for(var b=a.length,d=0;d<b;d++){var e=a[d];e.id!==ia&&(e.className="calendar calendar-hidden")}Ba?(G.className="calendar calendar-hidden",da(),z("onDatePickerClosed",ia)):(G.className="calendar calendar-shown",
+Z(new Date(Ib),!db),z("onDatePickerOpened",ia));Ba=!Ba}function Li(a){a.keyCode===P.escape&&Ba&&Ec()}function Ec(){Ba&&(G.className="calendar calendar-hidden",Ba=!1,da(),z("onDatePickerClosed",ia))}function Vf(a){cc.value=oh(c.datePickerSelectedDateFormat,a);yd.value=N(a.getDate())+"/"+N(a.getMonth())+"/"+a.getFullYear()}function Bh(a){var b=!0;null!==c.minimumDatePickerDate&&(b=Jd(c.minimumDatePickerDate,a));b&&null!==c.maximumDatePickerDate&&(b=Jd(a,c.maximumDatePickerDate));return b}function cj(a){Sc=
+k("span","year-dropdown-button");Sc.ondblclick=D;Sc.onclick=ej;a.appendChild(Sc);cg=k("span");Sc.appendChild(cg);Te=k("span","ib-arrow-down-full-medium");Sc.appendChild(Te)}function dj(a){var b=new Date(c.minimumYear,1,1);gb=k("div","years-drop-down");a.appendChild(gb);Ue=k("div","contents custom-scroll-bars");for(gb.appendChild(Ue);!(fj(b.getFullYear()),lc(b),b.getFullYear()>c.maximumYear););}function fj(a){var b=k("div");b.className="year";b.innerText=a.toString();b.id="year-selected-"+a.toString();
+Ue.appendChild(b);b.ondblclick=D;b.onclick=function(d){D(d);A.getFullYear()!==a&&(A.setFullYear(a),Z(A),sg())}}function ej(a){D(a);"block"!==gb.style.display?(da(),gb.style.display="block",Te.className="ib-arrow-up-full-medium",a=gj(),Ue.scrollTop=null!==a?a.offsetTop-gb.offsetHeight/2:0):sg()}function gj(){var a=gb.getElementsByClassName("year"),b=a.length;if(1<=b)for(var d=0;d<b;d++)"year"!==a[d].className&&(a[d].className="year");a=tb("year-selected-"+A.getFullYear());null!==a&&(a.className+=" year-selected");
+if(!t){var e=[];Sa(function(f){f=f.from.getFullYear();if(-1===e.indexOf(f)){var g=tb("year-selected-"+f);null!==g&&-1===g.className.indexOf(" year-selected")&&(g.className+=" year-has-events");e.push(f)}})}return a}function sg(){Ve()&&(Te.className="ib-arrow-down-full-medium",gb.style.display="none")}function Ve(){return null!==gb&&"block"===gb.style.display}function fh(a){var b=(a=r(a)?a:!0)?B.body.addEventListener:B.body.removeEventListener,d=a?B.addEventListener:B.removeEventListener;a=a?Ga.addEventListener:
+Ga.removeEventListener;b("click",hj);b("contextmenu",da);b("mousemove",ij);b("mouseleave",jj);d("scroll",da);d("keydown",kj);a("resize",da);a("resize",tg);a("resize",lj);a("blur",mj)}function hj(a){da();aa(a)||xb()}function mj(){da();Tc();t&&Ec()}function lj(){mc(Ca.windowResize);Uc(Ca.windowResize,function(){ta(!0,!1)},50,!1)}function da(){We(va);We(T);We(Da);We(Ta);Xe();sg();Tc()}function kj(a){if(t)Ba&&(aa(a)&&a.keyCode===P.left?(a.preventDefault(),Le()):aa(a)&&a.keyCode===P.right?(a.preventDefault(),
+Ne()):a.keyCode===P.left?Dd():a.keyCode===P.right?Fd():a.keyCode===P.down&&Me());else if(wd())a.keyCode===P.escape&&wd()&&xd();else{if(ic){var b=Ch();aa(a)&&a.keyCode===P.left&&b?(a.preventDefault(),Le()):aa(a)&&a.keyCode===P.right&&b?(a.preventDefault(),Ne()):a.keyCode===P.escape?!Dh()&&b&&c.useEscapeKeyToExitFullScreenMode&&eb():a.keyCode===P.left&&b?Dd():a.keyCode===P.right&&b?Fd():a.keyCode===P.down&&b?Me():a.keyCode===P.f5&&b&&ta(!1,!0)}else a.keyCode===P.escape&&Dh();if(aa(a)&&a.shiftKey&&a.keyCode===
+P.a)a.preventDefault(),c.manualEditingEnabled&&U(null,new Date);else if(aa(a)&&a.shiftKey&&a.keyCode===P.c)a.preventDefault(),Eh();else if(aa(a)&&a.shiftKey&&a.keyCode===P.e){if(a.preventDefault(),c.exportEventsEnabled){a=W(ma);b=W(oa);var d=W(wa);a=a?Ua:b?Va:d?Wa:Mc;0<a.length&&Lc(a)}}else if(aa(a)&&a.shiftKey&&a.keyCode===P.f)a.preventDefault(),u.enabled&&(0<Ua.length||0<Mc.length||0<Va.length||0<Wa.length)&&Ed();else if(aa(a)&&a.shiftKey&&a.keyCode===P.m){a.preventDefault();if(W(oa))for(a=Ye.length,
+b=0;b<a;b++)Ye[b]();if(W(wa))for(a=Ze.length,b=0;b<a;b++)Ze[b]()}else aa(a)&&a.shiftKey&&a.keyCode===P.v?(a.preventDefault(),a=X.length,W(ma)&&0<a&&ug(J,hb)):aa(a)&&a.shiftKey&&a.keyCode===P.x?(a.preventDefault(),Eh(!0)):aa(a)&&a.shiftKey&&a.keyCode===P.f11&&(a.preventDefault(),eb())}}function aa(a){return a.ctrlKey||a.metaKey}function Dh(){var a=!1;da();xb();0<La.length&&(a=La[La.length-1],"function"===typeof a&&(La.pop(),a(!1)),a=!0);!a&&(W(ma)||W(oa)||W(wa))&&(nc(ma),nc(oa),nc(wa),Ua=[],Vc=[],
+Va=[],Wa=[],$e(),a=!0);a||(a=Id());!a&&0<X.length&&(Wc(),X=[],hb=!1);return a}function pa(a,b){return a.getDate()===b.getDate()&&a.getMonth()===b.getMonth()&&a.getFullYear()===b.getFullYear()}function nj(a,b){return a.getMonth()===b.getMonth()&&a.getFullYear()===b.getFullYear()}function Jd(a,b){var d=new Date(a.getFullYear(),a.getMonth(),a.getDate());d.setHours(0,0,0,0);var e=new Date(b.getFullYear(),b.getMonth(),b.getDate());e.setHours(0,0,0,0);return d<=e}function Xc(a){var b=new Date;return null!==
+a&&a.getDate()===b.getDate()&&a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()}function vg(a){var b=("0"+a.getDate()).slice(-2),d=("0"+a.getMonth()).slice(-2);return b+"/"+d+"/"+a.getFullYear()}function af(a){return N(a.getHours())+":"+N(a.getMinutes())}function Ma(a){return 0>a.getDay()-1?6:a.getDay()-1}function vd(a,b){return(new Date(a,b+1,0)).getDate()}function wg(a){var b=c.thText;if(31===a||21===a||1===a)b=c.stText;else if(22===a||2===a)b=c.ndText;else if(23===a||3===a)b=c.rdText;
+return b}function xg(a){var b=a.getHours();a=a.getMinutes();return 60*b+a}function Ob(a,b){var d=new Date(a.getFullYear(),a.getMonth(),a.getDate()),e=new Date(b.getFullYear(),b.getMonth(),b.getDate());return Math.ceil(Math.abs(e-d)/864E5)}function Na(a,b){b=xa(b)?b:1;a.setDate(a.getDate()+b)}function ib(a,b){b=xa(b)?b:1;a.setDate(a.getDate()+7*b)}function Yc(a,b){b=xa(b)?b:1;a.setMonth(a.getMonth()+b)}function lc(a,b){b=xa(b)?b:1;a.setFullYear(a.getFullYear()+b)}function Pb(a,b){var d=[],e=Math.abs(b-
+a)/1E3,f=Math.floor(e/86400);e-=86400*f;0<f&&d.push(f.toString()+n.space+(1===f?c.dayText:c.daysText));f=Math.floor(e/3600)%24;e-=3600*f;0<f&&d.push(f.toString()+n.space+(1===f?c.hourText:c.hoursText));e=Math.floor(e/60)%60;0<e&&d.push(e.toString()+n.space+(1===e?c.minuteText:c.minutesText));return d.join(", ")}function Zc(a,b){if(r(a)){var d=("0"+a.getDate()).slice(-2),e=("0"+(a.getMonth()+1)).slice(-2);b.value="date"===b.type?a.getFullYear()+"-"+e+"-"+d:d+"/"+e+"/"+a.getFullYear()}}function Kd(a,
+b){var d=void 0!==b?b:new Date;if(a.value!==n.empty)if("date"===a.type)d=new Date(a.value+"T00:00:00Z");else{var e=a.value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);e&&(e=new Date(e[3],e[2]-1,e[1],0,0,0,0),e instanceof Date&&!isNaN(e)&&(d=e))}r(d)&&(d=new Date(d.getTime()+Math.abs(6E4*d.getTimezoneOffset())));return d}function Fh(a,b){if("date"===a.type){var d=("0"+b.getDate()).slice(-2),e=("0"+(b.getMonth()+1)).slice(-2);a.setAttribute("min",b.getFullYear()+"-"+e+"-"+d)}}function bf(a,b){var d=0,
+e=0,f=b.split(":");if(2===f.length){var g=parseInt(f[0]);f=parseInt(f[1]);!isNaN(g)&&2>=g.toString().length&&(d=g);!isNaN(f)&&2>=f.toString().length&&(e=f)}a.setHours(d);a.setMinutes(e)}function $c(a,b){return new Date(a.getTime()+6E4*b)}function oh(a,b){var d=a,e=Ma(b);d=d.replace("{dddd}",c.dayNames[e]);d=d.replace("{ddd}",c.dayNamesAbbreviated[e]);d=d.replace("{dd}",N(b.getDate()));d=d.replace("{d}",b.getDate());d=d.replace("{o}",wg(b.getDate()));d=d.replace("{mmmm}",c.monthNames[b.getMonth()]);
+d=d.replace("{mmm}",c.monthNamesAbbreviated[b.getMonth()]);d=d.replace("{mm}",N(b.getMonth()+1));d=d.replace("{m}",b.getMonth()+1);d=d.replace("{yyyy}",b.getFullYear());d=d.replace("{yyy}",b.getFullYear().toString().substring(1));d=d.replace("{yy}",b.getFullYear().toString().substring(2));return d=d.replace("{y}",parseInt(b.getFullYear().toString().substring(2)).toString())}function ra(){for(var a=0;6>a;a++)for(var b=0;7>b;b++){var d=tb("calendar-day-"+(7*a+(b+1)));null!==d&&(Qb(d,"event"),Qb(d,"plus-x-events"))}Gh();
+Mc=[];a=Nb(Jc());b=a.length;for(d=0;d<b;d++){var e=a[d];if(null!==cf(e.from)){var f=e;yg(f.from,f);if(f.from.getDate()!==f.to.getDate()){var g=Ob(f.from,f.to);if(0<g)for(var h=new Date(f.from),l=0;l<g;l++)Na(h),null!==cf(h)&&yg(h,f)}}ad(e)&&Mc.push(e);f=I(e.repeatEvery);f>x.never&&(f===x.everyDay?yb(e,Na,1):f===x.everyWeek?yb(e,ib,1):f===x.every2Weeks?yb(e,ib,2):f===x.everyMonth?yb(e,Yc,1):f===x.everyYear?yb(e,lc,1):f===x.custom&&(f=I(e.repeatEveryCustomType),g=I(e.repeatEveryCustomValue),0<g&&(f===
+Q.daily?yb(e,Na,g):f===Q.weekly?yb(e,ib,g):f===Q.monthly?yb(e,Yc,g):f===Q.yearly&&yb(e,lc,g))))}a=Mc.length;c.exportEventsEnabled&&jb(eg,0<a);null!==Ke&&jb(Ke,0<a);Hh()}function yb(a,b,d){for(var e=new Date(a.from),f=Rb(a.repeatEveryExcludeDays);e<Yf;){b(e,d);var g=!(!r(a.repeatEnds)||Jd(e,a.repeatEnds));-1!==f.indexOf(e.getDay())||g||null!==cf(e)&&yg(e,a)}}function yg(a,b){var d=cf(a),e=Rb(b.seriesIgnoreDates),f=vg(a);if(null!==d&&ad(b)&&-1===e.indexOf(f)&&(oj(a,b),!t))if(d.getElementsByClassName("event").length<
+c.maximumEventsPerDayDisplay||0>=c.maximumEventsPerDayDisplay||c.useOnlyDotEventsForMainDisplay){e=k("div","event");var g=b.title;e.setAttribute("event-type",I(b.type));e.setAttribute("event-id",b.id);c.showTimesInMainCalendarEvents&&!b.isAllDay&&b.from.getDate()===b.to.getDate()&&(g=Ld(b.from,b.to)+": "+g);if(c.useOnlyDotEventsForMainDisplay)e.className+=" event-circle";else{if(I(b.repeatEvery)>x.never){var h=k("div","ib-refresh-small ib-no-hover ib-no-active");h.style.borderColor=e.style.color;
+e.appendChild(h)}e.innerHTML+=bd(g)}d.appendChild(e);zg(e,b,a,d);df(e,b,Ag(b,a),c.applyCssToEventsNotInCurrentMonth);ef(e,b);pa(b.from,a)&&(e.id="day-"+b.id);e.onmousemove=function(l){null!==Md&&Md.id===b.id?D(l):Ih(l,b)};e.oncontextmenu=function(l){ff(l,b,f)};e.addEventListener("click",function(l){gf(l,b)});Xa("onEventClick")&&e.addEventListener("click",function(){z("onEventClick",b)});c.manualEditingEnabled?e.ondblclick=function(l){D(l);U(b)}:Xa("onEventDoubleClick")&&(e.ondblclick=function(){z("onEventDoubleClick",
+b)})}else pj(d,a)}function pj(a,b){var d=a.getElementsByClassName("plus-x-events");d=0<d.length?d[0]:null;if(null===d){var e=new Date(b);d=k("div","plus-x-events");d.setAttribute("events","1");d.ondblclick=D;a.appendChild(d);if(c.applyCssToEventsNotInCurrentMonth&&b.getMonth()!==A.getMonth()||b.getFullYear()!==A.getFullYear())d.className+=" day-muted";y(d,"+1 "+c.moreText);d.onclick=function(){Sb(e,!0)}}else{var f=parseInt(d.getAttribute("events"))+1;d.setAttribute("events",f.toString());y(d,"+"+
+f+n.space+c.moreText)}}function cf(a){var b=new Date(A.getFullYear(),A.getMonth(),1);var d=new Date(A);var e=new Date(A),f=null,g=0;b=Ma(b);d.setMonth(d.getMonth()+1);e.setMonth(e.getMonth()-1);a.getMonth()===d.getMonth()&&a.getFullYear()===d.getFullYear()?(d=b+vd(A.getFullYear(),A.getMonth()),g=a.getDate()+d):a.getMonth()===e.getMonth()&&a.getFullYear()===e.getFullYear()?g=b-Ob(a,A)+1:a.getMonth()===A.getMonth()&&a.getFullYear()===A.getFullYear()&&(d=b,g=a.getDate()+d);0<g&&(f=tb("calendar-day-"+
+g));return f}function Qb(a,b){for(var d=a.getElementsByClassName(b);d[0];)d[0].parentNode.removeChild(d[0])}function qj(a,b){for(var d=a.getElementsByClassName(b),e=d.length,f=0;f<e;f++)d[f].style.display="block"}function Jh(a,b){for(var d=a.getElementsByClassName(b);d[0];)d[0].className=d[0].className.replace(b,n.empty)}function Ag(a,b){var d=I(a.repeatEvery),e=new Date(a.to);d>x.never&&(d=new Date(b),d.setHours(e.getHours(),e.getMinutes()),e=d);return e}function Pi(){if(!t){var a=null!==ma;a&&(ma.innerHTML=
+n.empty);a||(ma=k("div","full-day-view"),G.appendChild(ma));a=k("div","title-bar");ma.appendChild(a);c.fullScreenModeEnabled&&(a.ondblclick=eb);Nd=k("div","title");a.appendChild(Nd);w(a,"ib-arrow-right-full",c.nextDayTooltipText,rj);w(a,"ib-close",c.closeTooltipText,sj);c.manualEditingEnabled&&c.showExtraToolbarButtons&&w(a,"ib-plus",c.addEventTooltipText,function(){if(c.useTemplateWhenAddingNewEvent){var e=oc(J,J);U(e);pc()}else Kc()});if(!t){w(a,"ib-hamburger",c.showMenuTooltipText,Cd);var b=k("div",
+"side-menu-button-divider-line");a.appendChild(b)}w(a,"ib-arrow-left-full",c.previousDayTooltipText,tj);c.exportEventsEnabled&&c.showExtraToolbarButtons&&(Kh=w(a,"ib-arrow-down-full-line",c.exportEventsTooltipText,function(){Lc(Ua)}));c.showExtraToolbarButtons&&(Lh=w(a,"ib-pin",c.todayTooltipText,uj),w(a,"ib-refresh",c.refreshTooltipText,function(){ta(!0,!0)}),u.enabled&&(Mh=w(a,"ib-search",c.searchTooltipText,Ed)),c.fullScreenModeEnabled&&(Oe=w(a,"ib-arrow-expand-left-right",c.enableFullScreenTooltipText,
+eb)));Ya=k("div","contents custom-scroll-bars");ma.appendChild(Ya);Ya.oncontextmenu=function(e){var f=Nh(e);hf=N(f[0])+":"+N(f[1]);null!==Da&&(aa(e)||xb(),null!==jf&&(f=0<X.length?"block":"none",Bg.style.display=f,jf.style.display=f),null!==Ua&&(f=0<Ua.length?"block":"none",Cg.style.display=f,Dg.style.display=f),da(),D(e),Od(e,Da))};qc=k("div","content-events-all-day");Ya.appendChild(qc);a=k("div","all-day-text");y(a,c.allDayText);qc.appendChild(a);ca=k("div","contents-events");ca.ondblclick=vj;Ya.appendChild(ca);
+c.manualEditingEnabled&&c.dragAndDropForEventsEnabled&&(ca.ondragover=D,ca.ondragenter=D,ca.ondragleave=D,ca.ondrop=wj);for(a=0;24>a;a++){b=k("div","hour");ca.appendChild(b);var d=k("div","hour-text");d.innerText=N(a)+":00";b.appendChild(d);d=k("div","hour-text");d.innerText=N(a)+":30";b.appendChild(d)}zb=k("div","time-arrow");ca.appendChild(zb);zb.appendChild(k("div","arrow-left"));zb.appendChild(k("div","line"))}}function vj(a){c.manualEditingEnabled&&(a=Nh(a),c.useTemplateWhenAddingNewEvent?(a=
+N(a[0])+":"+N(a[1]),a=oc(J,J,a,a),U(a),pc()):U(null,J,a))}function Sb(a,b){b=r(b)?b:!1;var d=Ma(new Date);d=-1<c.visibleDays.indexOf(d);Nd.innerHTML=n.empty;J=new Date(a);Ua=[];Vc=[];qc.style.display="block";jb(Lh,d);Qb(Ya,"event");Eg(ma);rc(Nd,a,!1,!0,!0);d=Oh(a);var e=[];null!==d&&R(Nd," ("+d+")","light-title-bar-text");Sa(function(p){for(var q=Ob(p.from,p.to)+1,v=new Date(p.from),E=0;E<q;E++){if(pa(v,a)){e.push(p);break}Na(v)}q=I(p.repeatEvery);q>x.never&&(q===x.everyDay?Ab(p,e,a,Na,1):q===x.everyWeek?
+Ab(p,e,a,ib,1):q===x.every2Weeks?Ab(p,e,a,ib,2):q===x.everyMonth?Ab(p,e,a,Yc,1):q===x.everyYear?Ab(p,e,a,lc,1):q===x.custom&&(q=I(p.repeatEveryCustomType),v=I(p.repeatEveryCustomValue),0<v&&(q===Q.daily?Ab(p,e,a,Na,v):q===Q.weekly?Ab(p,e,a,ib,v):q===Q.monthly?Ab(p,e,a,Yc,v):q===Q.yearly&&Ab(p,e,a,lc,v))))});e=Nb(e);d=e.length;for(var f=null,g=Ph(),h=0;h<d;h++){var l=xj(e[h],a);null===f&&(f=l)}b&&(Xc(J)&&W(ma)&&c.showTimelineArrowOnFullDayView?Ya.scrollTop=g:Ya.scrollTop=f-Ya.offsetHeight/2);1>=qc.offsetHeight&&
+(qc.style.display="none");c.exportEventsEnabled&&jb(Kh,0<Ua.length);jb(Mh,0<Ua.length);yj();Qh()}function sj(){nc(ma);$e();J=null;Ua=[];Vc=[]}function Ab(a,b,d,e,f){for(var g=new Date(a.from),h=Rb(a.repeatEveryExcludeDays);g<d;){e(g,f);var l=!(!r(a.repeatEnds)||Jd(g,a.repeatEnds));if(-1===h.indexOf(g.getDay())&&!l&&pa(g,d)){b.push(a);break}}}function xj(a,b){var d=0,e=Rb(a.seriesIgnoreDates),f=vg(b);if(ad(a)&&-1===e.indexOf(f)){var g=k("div","event");g.ondblclick=D;g.setAttribute("event-type",I(a.type));
+g.setAttribute("event-id",a.id);g.onclick=function(l){D(l);l=null;Ga.getComputedStyle?l=document.defaultView.getComputedStyle(g,null).getPropertyValue("z-index"):g.currentStyle&&(l=g.currentStyle["z-index"]);l=null===l||"auto"===l?1:parseInt(l)+1;g.style.zIndex=l.toString()};a.isAllDay?qc.appendChild(g):(c.manualEditingEnabled&&c.dragAndDropForEventsEnabled&&(pa(a.from,a.to)&&(g.className+=" resizable",g.onmousedown=$e,g.onmouseup=Qh),g.ondragstart=function(l){var p=Fg(g);Pd=g;sc=a;Gg=p.top-l.pageY},
+g.setAttribute("draggable",!0)),ca.appendChild(g));g.oncontextmenu=function(l){ff(l,a,f)};df(g,a,Ag(a,b));ef(g,a);pa(a.from,b)&&(g.id="full-day-"+a.id);e=k("div","title");if(I(a.repeatEvery)>x.never){var h=k("div","ib-refresh-medium ib-no-hover ib-no-active");h.style.borderColor=g.style.color;e.appendChild(h)}e.innerHTML+=bd(a.title);g.appendChild(e);if(!a.isAllDay||c.showAllDayEventDetailsInFullDayView)e=k("div","date"),g.appendChild(e),h=k("div","duration"),g.appendChild(h),a.from.getDate()===a.to.getDate()?
+a.isAllDay?y(e,c.allDayText):(y(e,Ld(a.from,a.to)),y(h,Pb(a.from,a.to))):(kf(e,a.from,a.to),y(h,Pb(a.from,a.to))),h.innerHTML===n.empty&&g.removeChild(h),xa(a.repeatEvery)&&a.repeatEvery>x.never&&(e=k("div","repeats"),y(e,c.repeatsText.replace(":",n.empty)+n.space+cd(a.repeatEvery)),g.appendChild(e)),Oa(a.location)&&(e=k("div","location"),y(e,a.location),g.appendChild(e)),Oa(a.description)&&(e=k("div","description"),y(e,a.description),g.appendChild(e));g.addEventListener("click",function(l){gf(l,
+a)});Xa("onEventClick")&&g.addEventListener("click",function(){z("onEventClick",a)});c.manualEditingEnabled?g.ondblclick=function(l){D(l);U(a)}:Xa("onEventDoubleClick")&&(g.ondblclick=function(){z("onEventDoubleClick",a)});a.isAllDay||(d=zj(b,g,a));Ua.push(a);a.isAllDay||Vc.push({eventDetails:a,eventElement:g,height:g.offsetHeight})}return d}function zj(a,b,d){var e=ca.offsetHeight,f=ca.offsetHeight/1440,g=c.spacing,h=null;if(!d.isAllDay){h=I(d.repeatEvery);if(pa(d.from,a)||h>x.never)g=f*xg(d.from);
+h=pa(d.to,a)||h>x.never?f*xg(d.to)-g:e;h-=2*c.spacing}b.style.top=g+"px";null!==h&&(b.style.height=h+"px");b.offsetTop+b.offsetHeight>e-c.spacing&&(b.style.height=e-b.offsetTop-3*c.spacing+"px");a=g+Ya.offsetHeight/2;a<=Ya.offsetHeight&&(a=0);return a}function tj(){J.setDate(J.getDate()-1);if(7>c.visibleDays.length)for(var a=Ma(J);-1===c.visibleDays.indexOf(a);)J.setDate(J.getDate()-1),a=Ma(J);Sb(J,!0)}function rj(){Na(J);if(7>c.visibleDays.length)for(var a=Ma(J);-1===c.visibleDays.indexOf(a);)Na(J),
+a=Ma(J);Sb(J,!0)}function uj(){J=new Date;Sb(J,!0)}function Nh(a){var b=Fg(ca);a=Math.floor((a.pageY-b.top)/(ca.offsetHeight/1440))/60;b=Math.floor(a);return[b,Math.round(60*(a-b))]}function wj(a){D(a);if(null===Pd)0===a.dataTransfer.files.length?Rh(a,J.getFullYear(),J.getMonth(),J.getDate()):Sh(a);else{var b=ca.offsetHeight/1440,d=Fg(ca);a=Math.abs(a.pageY)-d.top+Gg;b=(a-Pd.offsetTop)/b;Pd.style.top=a+"px";sc.from=$c(sc.from,b);sc.to=$c(sc.to,b);z("onEventUpdated",sc);sc=Pd=null;Gg=0;ta()}}function Qh(){$e();
+c.manualEditingEnabled&&Uc(Ca.fullDayEventSizeTracking,function(){var a=Vc.length;if(0<a){for(var b=ca.offsetHeight/1440,d=!1,e=0;e<a;e++){var f=Vc[e];f.height!=f.eventElement.offsetHeight&&(d=(f.eventElement.offsetHeight-f.height)/b,f.height=f.eventElement.offsetHeight,f.eventDetails.to=$c(f.eventDetails.to,d),d=!0,z("onEventUpdated",f.eventDetails))}d&&ta()}},50)}function $e(){c.manualEditingEnabled&&mc(Ca.fullDayEventSizeTracking)}function yj(){var a=ca.getElementsByClassName("event");a=[].slice.call(a);
+var b=a.length;if(1<b){a.sort(Aj);for(var d=0;d<b;d++)for(var e=a[d],f=0;f<b;f++)if(f!==d){var g=a[f],h=!0,l=e.offsetLeft,p=e.offsetTop,q=e.offsetWidth,v=g.offsetLeft,E=g.offsetTop,ea=g.offsetHeight,Tb=g.offsetWidth;if(p+e.offsetHeight<E||p>E+ea||l+q<v||l>v+Tb)h=!1;h&&(h=C(e.getAttribute("event-position")),l=C(g.getAttribute("event-position")),h===n.empty&&l===n.empty?(lf(e),lf(g),g.style.left=e.offsetLeft+e.offsetWidth+c.spacing+"px",e.setAttribute("event-position","left"),g.setAttribute("event-position",
+"right")):h===n.empty&&"right"===l?(lf(e),e.setAttribute("event-position","left"),g.setAttribute("event-position","right")):h===n.empty&&"left"===l&&(e.style.left=g.offsetLeft+g.offsetWidth+c.spacing+"px",lf(e),e.setAttribute("event-position","right"),g.setAttribute("event-position","left")))}}}function lf(a){a.style.width=a.offsetWidth/2-(3*c.spacing+c.spacing/4)+"px"}function Aj(a,b){var d=0;a.offsetTop<b.offsetTop?d=-1:a.offsetTop>b.offsetTop&&(d=1);return d}function Ph(){var a=0;null!==zb&&(Xc(J)&&
+W(ma)&&c.showTimelineArrowOnFullDayView?(a=ca.offsetHeight/1440*xg(new Date),zb.style.display="block",zb.style.top=a-zb.offsetHeight/2+"px"):zb.style.display="none");return a}function Ni(){if(!t){var a=null!==oa;a&&(oa.innerHTML=n.empty);a||(oa=k("div","list-all-events-view"),G.appendChild(oa));a=k("div","title-bar");oa.appendChild(a);c.fullScreenModeEnabled&&(a.ondblclick=eb);var b=k("div","title");y(b,c.allEventsText);a.appendChild(b);w(a,"ib-close",c.closeTooltipText,function(){Va=[];nc(oa)});
+c.showExtraToolbarButtons&&(c.manualEditingEnabled&&w(a,"ib-plus",c.addEventTooltipText,Kc),c.exportEventsEnabled&&(Th=w(a,"ib-arrow-down-full-line",c.exportEventsTooltipText,function(){Lc(Va)})),t||(w(a,"ib-hamburger",c.showMenuTooltipText,Cd),b=k("div","side-menu-button-divider-line"),a.appendChild(b)),w(a,"ib-refresh",c.refreshTooltipText,function(){ta(!0,!0)}),u.enabled&&(Uh=w(a,"ib-search",c.searchTooltipText,Ed)),c.fullScreenModeEnabled&&(Pe=w(a,"ib-arrow-expand-left-right",c.enableFullScreenTooltipText,
+eb)));dd=k("div","contents custom-scroll-bars");oa.appendChild(dd)}}function rh(a){a=r(a)?a:!1;Eg(oa);dd.innerHTML=n.empty;Va=[];Ye=[];a&&(dd.scrollTop=0);a=Nb(Jc());for(var b=a.length,d=0;d<b;d++)Bj(a[d]);c.exportEventsEnabled&&jb(Th,0<Va.length);jb(Uh,0<Va.length);0===Va.length&&Vh(dd,Kc)}function Bj(a){if(ad(a)){var b=Cj(a.from),d=k("div","event");b.appendChild(d);d.oncontextmenu=function(f){ff(f,a)};zg(d,a,a.from,b);df(d,a);ef(d,a);d.id="month-"+a.id;d.setAttribute("event-type",I(a.type));d.setAttribute("event-id",
+a.id);b=k("div","title");if(I(a.repeatEvery)>x.never){var e=k("div","ib-refresh-medium ib-no-hover ib-no-active");e.style.borderColor=d.style.color;b.appendChild(e)}b.innerHTML+=bd(a.title);d.appendChild(b);b=k("div","date");d.appendChild(b);e=k("div","duration");d.appendChild(e);a.from.getDate()===a.to.getDate()?a.isAllDay?mf(b,a.from,null," - "+c.allDayText):(mf(b,a.from,null," - "+Ld(a.from,a.to)),y(e,Pb(a.from,a.to))):(kf(b,a.from,a.to),y(e,Pb(a.from,a.to)));e.innerHTML===n.empty&&d.removeChild(e);
+xa(a.repeatEvery)&&a.repeatEvery>x.never&&(b=k("div","repeats"),y(b,c.repeatsText.replace(":",n.empty)+n.space+cd(a.repeatEvery)),d.appendChild(b));Oa(a.location)&&(b=k("div","location"),y(b,a.location),d.appendChild(b));Oa(a.description)&&(b=k("div","description"),y(b,a.description),d.appendChild(b));d.addEventListener("click",function(f){gf(f,a)});Xa("onEventClick")&&d.addEventListener("click",function(){z("onEventClick",a)});c.manualEditingEnabled?d.ondblclick=function(f){D(f);U(a)}:Xa("onEventDoubleClick")&&
+(d.ondblclick=function(){z("onEventDoubleClick",a)});Va.push(a)}}function Cj(a){var b="month-"+a.getMonth()+"-"+a.getFullYear(),d=tb(b);if(null===d){var e=new Date(a),f=function(){Va=[];nc(oa);Z(e)},g=k("div","month");dd.appendChild(g);var h=k("div","header");y(h,c.monthNames[a.getMonth()]+n.space+a.getFullYear());h.ondblclick=f;g.appendChild(h);w(h,"ib-arrow-expand-left-right",c.expandMonthTooltipText,f);if(c.manualEditingEnabled){var l=new Date(a.getFullYear(),a.getMonth(),1);w(h,"ib-plus",c.addEventTooltipText,
+function(){if(c.useTemplateWhenAddingNewEvent){var q=oc(l,l);U(q);pc()}else U(null,l)})}c.manualEditingEnabled&&w(h,"ib-close",c.removeEventsTooltipText,function(){dg(e,nj)});f=function(){var q=p,v=d;"none"!==v.style.display?(v.style.display="none",q.className="ib-square-hollow",L.visibleAllEventsMonths[b]=!1,ua(q,c.restoreTooltipText)):(v.style.display="block",q.className="ib-minus",L.visibleAllEventsMonths[b]=!0,ua(q,c.minimizedTooltipText))};var p=w(h,"ib-minus",c.minimizedTooltipText,f);Ye.push(f);
+d=k("div","events");d.id=b;g.appendChild(d);L.visibleAllEventsMonths.hasOwnProperty(b)&&!L.visibleAllEventsMonths[b]&&(d.style.display="none",p.className="ib-square-hollow",ua(p,c.restoreTooltipText));nf(d,a.getFullYear(),a.getMonth(),a.getDate())}return d}function Oi(){if(!t){var a=null!==wa;a&&(wa.innerHTML=n.empty);a||(wa=k("div","list-all-week-events-view"),G.appendChild(wa));a=k("div","title-bar");wa.appendChild(a);c.fullScreenModeEnabled&&(a.ondblclick=eb);Za=k("div","title");a.appendChild(Za);
+w(a,"ib-arrow-right-full",c.nextWeekTooltipText,Dj);w(a,"ib-close",c.closeTooltipText,function(){Wa=[];nc(wa)});c.manualEditingEnabled&&c.showExtraToolbarButtons&&w(a,"ib-plus",c.addEventTooltipText,Kc);if(!t){w(a,"ib-hamburger",c.showMenuTooltipText,Cd);var b=k("div","side-menu-button-divider-line");a.appendChild(b)}w(a,"ib-arrow-left-full",c.previousWeekTooltipText,Ej);c.showExtraToolbarButtons&&(c.exportEventsEnabled&&(Wh=w(a,"ib-arrow-down-full-line",c.exportEventsTooltipText,function(){Lc(Wa)})),
+w(a,"ib-pin",c.thisWeekTooltipText,Fj),w(a,"ib-refresh",c.refreshTooltipText,function(){ta(!0,!0)}),u.enabled&&(Xh=w(a,"ib-search",c.searchTooltipText,Ed)),c.fullScreenModeEnabled&&(Qe=w(a,"ib-arrow-expand-left-right",c.enableFullScreenTooltipText,eb)));ed=k("div","contents custom-scroll-bars");wa.appendChild(ed)}}function Nc(a,b){b=r(b)?b:!1;Eg(wa);ed.innerHTML=n.empty;fd={};of={};Qd={};Wa=[];Ze=[];Bb=null===a?new Date:new Date(a);b&&(ed.scrollTop=0);var d=a;d=r(d)?d:new Date;var e=0===d.getDay()?
+7:d.getDay();e=d.getDate()-e+1;var f=e+6,g=new Date(d);d=new Date(d);g.setDate(e);g.setHours(0,0,0,0);d.setDate(f);d.setHours(23,59,59,99);e=[g,d];d=e[0];e=e[1];f=new Date(d);do Hg(f),Na(f);while(f<e);Za.innerHTML=n.empty;if(c.showWeekNumbersInTitles){f=Za;g=c.weekText+n.space;var h=new Date(d.getFullYear(),0,1),l=Math.ceil(((d-h)/864E5+h.getDay()+1)/7);4<h.getDay()&&l--;R(f,g+l+": ")}d.getFullYear()===e.getFullYear()?(rc(Za,d,!1,!1),R(Za," - "),rc(Za,e,!1,!1),R(Za,", "+d.getFullYear())):(rc(Za,d,
+!1,!0),R(Za," - "),rc(Za,e,!1,!0));f=Nb(Jc());g=f.length;for(h=0;h<g;h++){l=f[h];for(var p=Ob(l.from,l.to)+1,q=new Date(l.from),v=!1,E=0;E<p;E++){if(q>=d&&q<=e){var ea=Hg(q),Tb=ea[0];ea=ea[1];null!==Tb&&null!==ea&&Yh(l,ea,Tb,q)&&(v=!0)}Na(q)}v&&Wa.push(l);q=I(l.repeatEvery);p=!1;q>x.never&&(q===x.everyDay?p=Cb(l,d,e,Na,1):q===x.everyWeek?p=Cb(l,d,e,ib,1):q===x.every2Weeks?p=Cb(l,d,e,ib,2):q===x.everyMonth?p=Cb(l,d,e,Yc,1):q===x.everyYear?p=Cb(l,d,e,lc,1):q===x.custom&&(q=I(l.repeatEveryCustomType),
+E=I(l.repeatEveryCustomValue),0<E&&(q===Q.daily?p=Cb(l,d,e,Na,E):q===Q.weekly?p=Cb(l,d,e,ib,E):q===Q.monthly?p=Cb(l,d,e,Yc,E):q===Q.yearly&&(p=Cb(l,d,e,lc,E)))));p&&!v&&Wa.push(l)}for(var Ub in fd)fd.hasOwnProperty(Ub)&&Qd.hasOwnProperty(Ub)&&(c.showEmptyDaysInWeekView||0<Qd[Ub].length)&&ed.appendChild(fd[Ub]);c.exportEventsEnabled&&jb(Wh,0<Wa.length);jb(Xh,0<Wa.length);0===Wa.length&&Vh(ed,Kc)}function Cb(a,b,d,e,f){for(var g=new Date(a.from),h=Rb(a.repeatEveryExcludeDays),l=!1;g<d;){e(g,f);var p=
+!(!r(a.repeatEnds)||Jd(g,a.repeatEnds));if(-1===h.indexOf(g.getDay())&&!p&&g>=b&&g<=d){var q=Hg(g);p=q[0];q=q[1];null!==p&&null!==q&&(Yh(a,q,p,g),l=!0)}}return l}function Yh(a,b,d,e){var f=!1,g=Ma(e)+e.getMonth()+e.getFullYear(),h=Rb(a.seriesIgnoreDates),l=vg(e);ad(a)&&-1===h.indexOf(l)&&(Qb(d,"no-events-text"),qj(b,"ib-close"),b=k("div","event"),b.setAttribute("event-type",I(a.type)),b.setAttribute("event-id",a.id),d.appendChild(b),Qd[g].push(b),b.oncontextmenu=function(p){ff(p,a,l)},zg(b,a,e,d),
+df(b,a,Ag(a,e)),ef(b,a),pa(a.from,e)&&(b.id="week-day-"+a.id),d=k("div","title"),I(a.repeatEvery)>x.never&&(e=k("div","ib-refresh-medium ib-no-hover ib-no-active"),e.style.borderColor=b.style.color,d.appendChild(e)),d.innerHTML+=bd(a.title),b.appendChild(d),d=k("div","date"),b.appendChild(d),e=k("div","duration"),b.appendChild(e),a.from.getDate()===a.to.getDate()?a.isAllDay?y(d,c.allDayText):(y(d,Ld(a.from,a.to)),y(e,Pb(a.from,a.to))):(kf(d,a.from,a.to),y(e,Pb(a.from,a.to))),e.innerHTML===n.empty&&
+b.removeChild(e),xa(a.repeatEvery)&&a.repeatEvery>x.never&&(d=k("div","repeats"),y(d,c.repeatsText.replace(":",n.empty)+n.space+cd(a.repeatEvery)),b.appendChild(d)),Oa(a.location)&&(d=k("div","location"),y(d,a.location),b.appendChild(d)),Oa(a.description)&&(d=k("div","description"),y(d,a.description),b.appendChild(d)),b.addEventListener("click",function(p){gf(p,a)}),Xa("onEventClick")&&b.addEventListener("click",function(){z("onEventClick",a)}),c.manualEditingEnabled?b.ondblclick=function(p){D(p);
+U(a)}:Xa("onEventDoubleClick")&&(b.ondblclick=function(){z("onEventDoubleClick",a)}),f=!0);return f}function Hg(a){var b=Ma(a),d=b+a.getMonth()+a.getFullYear(),e=null,f=null,g=new Date(a);if(!fd.hasOwnProperty(d)&&-1<c.visibleDays.indexOf(b)){var h=new Date(a),l=function(){Sb(h,!0)},p=function(){if(c.useTemplateWhenAddingNewEvent){var E=oc(h,h);U(E);pc()}else U(null,h)},q=k("div","day");fd[d]=q;Qd[d]=[];0<=c.weekendDays.indexOf(a.getDay())&&(q.className+=" weekend-day");0<=c.workingDays.indexOf(Ma(a))&&
+(q.className+=" working-day");f=k("div","header");f.ondblclick=l;q.appendChild(f);f.oncontextmenu=function(E){sh(E,b)};mf(f,a,c.dayNames[b]+n.space);a=Oh(a);null!==a&&R(f," ("+a+")","light-title-bar-text");w(f,"ib-arrow-expand-left-right",c.expandDayTooltipText,l);c.manualEditingEnabled&&w(f,"ib-plus",c.addEventTooltipText,p);c.manualEditingEnabled&&w(f,"ib-close",c.removeEventsTooltipText,function(){dg(g,pa)});l=function(){var E=v,ea=e;"none"!==ea.style.display?(ea.style.display="none",E.className=
+"ib-square-hollow",L.visibleWeeklyEventsDay[d]=!1,ua(E,c.restoreTooltipText)):(ea.style.display="block",E.className="ib-minus",L.visibleWeeklyEventsDay[d]=!0,ua(E,c.minimizedTooltipText))};var v=w(f,"ib-minus",c.minimizedTooltipText,l);Ze.push(l);e=k("div","events");q.appendChild(e);L.visibleWeeklyEventsDay.hasOwnProperty(d)&&!L.visibleWeeklyEventsDay[d]&&(e.style.display="none",v.className="ib-square-hollow",ua(v,c.restoreTooltipText));nf(e,h.getFullYear(),h.getMonth(),h.getDate());q=k("div","no-events-text");
+e.appendChild(q);R(q,c.noEventsAvailableText);c.manualEditingEnabled&&(R(q,n.space+c.clickText+n.space),R(q,c.hereText,"link",p),R(q,n.space+c.toAddANewEventText));of[d]=[e,f]}else of.hasOwnProperty(d)&&(f=of[d],e=f[0],f=f[1]);return[e,f]}function Ej(){Bb.setDate(Bb.getDate()-7);Nc(Bb,!0)}function Dj(){ib(Bb);Nc(Bb,!0)}function Fj(){Bb=new Date;Nc(Bb,!0)}function Ld(a,b){return Ig(a)+n.space+c.toTimeText+n.space+Ig(b)}function Ig(a){return N(a.getHours())+":"+N(a.getMinutes())}function kf(a,b,d){a.innerHTML=
+n.empty;rc(a,b);R(a,n.space+c.toTimeText+n.space);rc(a,d)}function rc(a,b,d,e,f){d=r(d)?d:!0;e=r(e)?e:!0;(f=r(f)?f:!1)&&R(a,c.dayNames[Ma(b)]+", ");mf(a,b);R(a,n.space+c.monthNames[b.getMonth()]);e&&R(a,n.space+b.getFullYear());d&&R(a,n.space+Ig(b))}function mf(a,b,d,e){r(d)&&R(a,d);R(a,b.getDate());c.showDayNumberOrdinals&&(d=k("sup"),d.innerText=wg(b.getDate()),a.appendChild(d));r(e)&&R(a,e)}function Xf(a,b,d,e,f,g){b=tb("calendar-day-"+b);if(null!==b){var h=new Date,l=a===h.getDate()&&e===h.getFullYear()&&
+d===h.getMonth();h=k("span");var p=new Date(e,d,a);f=f?" day-muted":n.empty;var q=!0;g=r(g)?g:!1;b.innerHTML=n.empty;b.className=b.className.replace(" cell-today",n.empty).replace(" cell-selected",n.empty).replace(" cell-no-click",n.empty);t&&l&&(b.className+=" cell-today");t&&!l&&null!==Ib&&pa(p,Ib)&&(b.className+=" cell-selected");t?(q=Bh(p),q||(b.className+=" cell-no-click",h.className="no-click")):h.className=n.empty;h.className+=f;h.className+=l&&!t?" today":n.empty;h.innerText=a;1!==a||t||(h.className+=
+" first-day");0<=c.weekendDays.indexOf(p.getDay())&&-1===b.className.indexOf("weekend-day")&&(b.className+=" weekend-day");0<=c.workingDays.indexOf(Ma(p))&&-1===b.className.indexOf("working-day")&&(b.className+=" working-day");b.oncontextmenu=function(v){if(!t&&null!==va){aa(v)||xb();tc=new Date(p);if(null!==pf){var E=0<X.length?"block":"none";Jg.style.display=E;pf.style.display=E}da();D(v);Od(v,va)}};c.showDayNumberOrdinals&&(l=k("sup"),l.innerText=wg(a),h.appendChild(l));b.appendChild(h);b.appendChild(k("span",
+"blank"));h=k("div","ib-arrow-expand-left-right-icon");b.appendChild(h);ua(h,c.expandDayTooltipText);h.onclick=function(){Sb(p,!0)};g&&c.showPreviousNextMonthNamesInMainDisplay&&R(b,c.monthNames[d],"month-name"+f,function(){1===a?Fd():Dd()},!0,!0);Gj(p,f,b);c.manualEditingEnabled&&(b.ondblclick=function(){if(c.useTemplateWhenAddingNewEvent){var v=oc(p,p);U(v);pc()}else U(null,p)},nf(b,e,d,a));t&&(b.onclick=q?function(v){D(v);Ve()?da():(v=new Date(p),v.setHours(0,0,0,0),Ec(),Vf(p),z("onDatePickerDateChanged",
+v),Ib=v)}:D);c.useOnlyDotEventsForMainDisplay&&b.appendChild(k("div","dots-separator"))}}function Oh(a){var b=null;if(c.showHolidays){for(var d=[],e=[],f=c.holidays.length,g=0;g<f;g++){var h=c.holidays[g],l=C(h.title,n.empty);I(h.day)===a.getDate()&&I(h.month)===a.getMonth()+1&&l!==n.empty&&e.indexOf(l.toLowerCase())&&(d.push(l),e.push(l.toLowerCase()))}0<d.length&&(b=d.join(", "))}return b}function Gj(a,b,d){if(c.showHolidays)for(var e=[],f=c.holidays.length,g=0;g<f;g++){var h=c.holidays[g],l=C(h.title,
+n.empty);I(h.day)===a.getDate()&&I(h.month)===a.getMonth()+1&&l!==n.empty&&e.indexOf(l.toLowerCase())&&(Hj(h,d,l,b),e.push(l.toLowerCase()))}}function Hj(a,b,d,e){var f=qf(a.onClick)||ja(a.onClickUrl)?"holiday-link":"holiday",g=a.onClick;ja(a.onClickUrl)&&(g=function(){Ga.open(a.onClickUrl,c.urlWindowTarget)});R(b,d,f+e,g,!0,!0)}function zg(a,b,d,e){if(!rf(b)&&c.dragAndDropForEventsEnabled&&c.manualEditingEnabled){var f=new Date(d),g=0<=c.weekendDays.indexOf(f.getDay())?" drag-not-allowed-weekend-day":
+" drag-not-allowed";a.setAttribute("draggable",!0);a.ondragstart=function(h){z("onEventDragStart",b);h.dataTransfer.setData("event_details",JSON.stringify(b));gd=f;Y=b;r(e)&&(e.className+=g,Ij(e));sf("cell",function(l){l.className+=" prevent-pointer-events"},a);sf("events",function(l){l.className+=" prevent-pointer-events"},a)};a.ondragend=function(){z("onEventDragStop",Y);Y=gd=null;r(e)&&(e.className=e.className.replace(g,n.empty),nf(e,f.getFullYear(),f.getMonth(),f.getDate()));sf("cell",function(h){h.className=
+h.className.replace(" prevent-pointer-events",n.empty)},a);sf("events",function(h){h.className=h.className.replace(" prevent-pointer-events",n.empty)},a)}}}function nf(a,b,d,e){if(c.dragAndDropForEventsEnabled&&c.manualEditingEnabled){var f=new Date(b,d,e);a.ondragover=function(g){Zh(g,a,f)};a.ondragenter=function(g){Zh(g,a,f)};a.ondragleave=function(g){$h(g,a,f)};a.ondrop=function(g){D(g);$h(g,a,f);if(0===g.dataTransfer.files.length){var h=e,l=new Date(b,d,h);if(null===Y||pa(gd,l))null===Y&&Rh(g,
+b,d,h);else{g=Y;if(null!==c&&Xa("onEventDragDrop"))c.onEventDragDrop(g,l);r(h)||(l=vd(b,d),h=Y.from.getDate(),h>l&&(h=l));var p=Ob(Y.from,gd);l=Ob(Y.from,Y.to);g=new Date(b,d,h,Y.from.getHours(),Y.from.getMinutes());h=new Date(b,d,h,Y.to.getHours(),Y.to.getMinutes());var q=Y.repeatEnds;0<p&&(g.setDate(g.getDate()-p),h.setDate(h.getDate()-p));r(q)&&(p=Ob(g,Y.from),g>Y.from?q.setDate(q.getDate()+p):q.setDate(q.getDate()-p));0<l&&h.setDate(h.getDate()+l);qa.updateEventDateTimes(Y.id,g,h,q);ta()}}else Sh(g)}}}
+function Ij(a){c.dragAndDropForEventsEnabled&&c.manualEditingEnabled&&(a.ondragover=null,a.ondragenter=null,a.ondragleave=null,a.ondrop=null)}function Zh(a,b,d){D(a);null===Y||-1!==b.className.indexOf(" drag-over")||pa(gd,d)||(b.className+=" drag-over")}function $h(a,b,d){D(a);null!==Y&&-1<b.className.indexOf(" drag-over")&&!pa(gd,d)&&(b.className=b.className.replace(" drag-over",n.empty))}function Rh(a,b,d,e){a=Kg(a.dataTransfer.getData("event_details"));if(null!==a){var f=new Date(a.from),g=new Date(a.to);
+a.from=new Date(b,d,e,f.getHours(),f.getMinutes(),0,0);a.to=new Date(b,d,e,g.getHours(),g.getMinutes(),0,0);qa.addEvent(a)}}function Sh(a){if(r(Ga.FileReader)){var b=new FileReader;b.onload=function(d){qa.addEventsFromJson(d.target.result)};b.readAsText(a.dataTransfer.files[0])}}function Kg(a){try{var b=JSON.parse(a)}catch(d){try{b=eval("("+a+")")}catch(e){console.error("Errors in object: "+d.message+", "+e.message),b=null}}return b}function Zi(){null!==va&&(Ka(B.body,va),pf=Jg=null);va=k("div","calendar-drop-down-menu");
+B.body.appendChild(va);c.manualEditingEnabled&&(na(va,"ib-plus-icon",c.addEventTitle+"...",function(){if(c.useTemplateWhenAddingNewEvent){var a=oc(tc,tc);U(a);pc()}else U(null,tc)},!0),Ha(va));na(va,"ib-arrow-expand-left-right-icon",c.expandDayTooltipText,function(){Sb(tc,!0)});Ha(va);na(va,"ib-hamburger-side-icon",c.viewWeekEventsText,function(){Nc(tc,!0)});c.manualEditingEnabled&&(Jg=Ha(va),pf=na(va,"ib-circle-icon",c.pasteText,function(){ug(tc,hb)}))}function $i(){null!==T&&(Ka(B.body,T),tf=uf=
+Rd=Sd=Td=Ud=Vd=Wd=Xd=Yd=Zd=$d=T=null);T=k("div","calendar-drop-down-menu");B.body.appendChild(T);c.manualEditingEnabled&&(Td=na(T,"ib-plus-icon",c.editEventTitle+"...",function(){U(Pa)},!0),Xd=Ha(T),Wd=na(T,"ib-pipe-icon",c.cutText,function(){Wc();hb=!0;ai(Pa);Wc(!1)}),Vd=Ha(T),Ud=na(T,"ib-circle-hollow-icon",c.copyText,function(){Wc();hb=!1;ai(Pa);Wc(!1)}),Zd=Ha(T),Yd=na(T,"ib-equals-icon",c.duplicateText+"...",function(){U(Pa);y(Vb,c.addEventTitle);ae.value=c.addText;vf.style.display="none";fa=
+hd(fa);w(Vb,"ib-close",c.closeTooltipText,id,!0)}),Sd=Ha(T),Rd=na(T,"ib-close-icon",c.removeEventText,function(){vb(B.body,Aa);var a=function(){Ka(B.body,Aa)},b=I(Pa.repeatEvery)>x.never&&null!==be;Je(c.confirmEventRemoveTitle,c.confirmEventRemoveMessage,function(){a();r(Pa.id)&&(He.checked||null===be?qa.removeEvent(Pa.id,!0):(za(Pa.seriesIgnoreDates)?Pa.seriesIgnoreDates.push(be):Pa.seriesIgnoreDates=[be],ra()),la())},a,b)}),$d=Ha(T));Lg=na(T,"ib-arrow-top-right-icon",c.openUrlText,function(){Mg(Pa.url)});
+c.exportEventsEnabled&&(uf=Ha(T),tf=na(T,"ib-arrow-down-full-line-icon",c.exportEventsTooltipText+"...",function(){Lc(ya)}))}function aj(){null!==Da&&(Ka(B.body,Da),jf=Bg=Dg=Cg=Da=null);c.manualEditingEnabled&&(Da=k("div","calendar-drop-down-menu"),B.body.appendChild(Da),na(Da,"ib-plus-icon",c.addEventTitle+"...",function(){if(c.useTemplateWhenAddingNewEvent){var a=oc(J,J,hf,hf);U(a);pc()}else U(null,J,hf)},!0),Cg=Ha(Da),Dg=na(Da,"ib-close-icon",c.removeEventsTooltipText,function(){dg(J,pa)}),Bg=
+Ha(Da),jf=na(Da,"ib-circle-icon",c.pasteText,function(){ug(J,hb)}))}function bj(){null===Ta&&(Ta=k("div","calendar-drop-down-menu"),B.body.appendChild(Ta),bi=na(Ta,"ib-close-icon",c.hideDayText,function(){c.visibleDays.splice(c.visibleDays.indexOf(ci),1);db=!1;z("onOptionsUpdated",c);Z(A,!0,!0)},!0),di=Ha(Ta),ei=na(Ta,"ib-rhombus-hollow-icon",c.showOnlyWorkingDaysText,function(){1<=c.workingDays.length&&(c.visibleDays=[].slice.call(c.workingDays),db=!1,z("onOptionsUpdated",c),Z(A,!0,!0))}),fi=Ha(Ta),
+na(Ta,"ib-octagon-hollow-icon",c.visibleDaysText+"...",Cd))}function na(a,b,d,e,f){f=r(f)?f:!1;var g=k("div","item");a.appendChild(g);g.appendChild(k("div",b));a=k("div","menu-text");y(a,d);g.appendChild(a);f&&(a.className+=" bold");g.onclick=function(){e()};return g}function Ha(a){var b=k("div","separator");a.appendChild(b);return b}function ff(a,b,d){if(null!==T){var e=C(b.url),f=rf(b);aa(a)||xb();Pa=b;be=r(d)?d:null;1<ya.length?(c.manualEditingEnabled&&(Td.style.display="none",Xd.style.display=
+"none",Wd.style.display="block",Vd.style.display="block",Ud.style.display="block",Zd.style.display="none",Yd.style.display="none",Sd.style.display="none",Rd.style.display="none"),$d.style.display="none",Lg.style.display="none",c.exportEventsEnabled&&(uf.style.display="block",tf.style.display="block")):(f?c.manualEditingEnabled&&(Td.style.display="block",Xd.style.display="none",Wd.style.display="none",Vd.style.display="none",Ud.style.display="none",Zd.style.display="none",Yd.style.display="none",Sd.style.display=
+"block",Rd.style.display="block",$d.style.display=e!==n.empty?"block":"none"):c.manualEditingEnabled&&(Td.style.display="block",Xd.style.display="block",Wd.style.display="block",Vd.style.display="block",Ud.style.display="block",Zd.style.display="block",Yd.style.display="block",Sd.style.display="block",Rd.style.display="block",$d.style.display=e!==n.empty?"block":"none"),Lg.style.display=e!==n.empty?"block":"none",c.exportEventsEnabled&&(uf.style.display="none",tf.style.display="none"));if(e!==n.empty||
+1<T.childElementCount)da(),D(a),Od(a,T)}}function sh(a,b){if(!t){aa(a)||xb();ci=b;var d=1<c.visibleDays.length?"block":"none",e=1<=c.workingDays.length?"block":"none";bi.style.display=d;di.style.display=d;ei.style.display=e;fi.style.display=e;da();D(a);Od(a,Ta)}}function We(a){jd(a)&&(a.style.display="none")}function jd(a){return null!==a&&"block"===a.style.display}function gi(){return jd(va)||jd(T)||jd(Da)||jd(Ta)||jd($a)}function Si(){if(!t&&null===Qa){Qa=k("div","calendar-dialog event-editor");
+B.body.appendChild(Qa);var a=k("div","view");Qa.appendChild(a);wf=k("div","disabled-area");a.appendChild(wf);Vb=k("div","title-bar");a.appendChild(Vb);Fc(Vb,Qa,null);var b=k("div","contents");a.appendChild(b);a=hi(b);kd(a,c.eventText,function(d){ld(d,Db,Qa)},!0);kd(a,c.typeText.replace(":",n.empty),function(d){ld(d,xf,Qa)});kd(a,c.repeatsText.replace(":",n.empty),function(d){ld(d,yf,Qa)});kd(a,c.optionalText,function(d){ld(d,Wb,Qa)});Db=md(b,!0,!1);xf=md(b,!1,!1);yf=md(b,!1,!1);Wb=md(b,!1,!1);Jj();
+Kj();Lj();a=k("div","buttons-container");b.appendChild(a);vf=sa(a,c.removeEventText,"remove",Mj);ae=sa(a,c.addText,"add-update",Nj);sa(a,c.cancelText,"cancel",id)}}function Jj(){ka(Db,c.titleText);var a=k("div","input-title-container");Db.appendChild(a);kb=k("input",null,"text");a.appendChild(kb);0<c.maximumEventTitleLength&&(kb.maxLength=c.maximumEventTitleLength);var b=function(){ii(null)};ji=sa(a,"...","select-colors",Oj,c.selectColorsText);ka(Db,c.fromText.replace(":",n.empty)+"/"+c.toText);a=
+k("div","split");Db.appendChild(a);lb=k("input");lb.onchange=b;a.appendChild(lb);ub(lb,"date");mb=k("input");a.appendChild(mb);ub(mb,"time");a=k("div","split");Db.appendChild(a);Ia=k("input");Ia.onchange=b;a.appendChild(Ia);ub(Ia,"date");nb=k("input");a.appendChild(nb);ub(nb,"time");nd=M(Db,c.isAllDayText,b)[0];ce=M(Db,c.showAlertsText)[0]}function Kj(){var a=k("div","radio-buttons-container");yf.appendChild(a);Eb=S(a,c.repeatsNever,"RepeatType",Xb);de=S(a,c.repeatsEveryDayText,"RepeatType",Xb);ee=
+S(a,c.repeatsEveryWeekText,"RepeatType",Xb);fe=S(a,c.repeatsEvery2WeeksText,"RepeatType",Xb);ge=S(a,c.repeatsEveryMonthText,"RepeatType",Xb);he=S(a,c.repeatsEveryYearText,"RepeatType",Xb);ob=S(a,c.repeatsCustomText,"RepeatType",Xb);zf=sa(a,"...","repeat-options",Pj,c.repeatOptionsTitle);a=k("div","split split-margin");yf.appendChild(a);Yb=k("input",null,"number");Yb.setAttribute("min","1");a.appendChild(Yb);var b=k("div","radio-buttons-container split-contents");a.appendChild(b);uc=S(b,c.dailyText,
+"RepeatCustomType");ie=S(b,c.weeklyText,"RepeatCustomType");je=S(b,c.monthlyText,"RepeatCustomType");ke=S(b,c.yearlyText,"RepeatCustomType")}function Lj(){var a=k("div","split");Wb.appendChild(a);ka(a,c.locationText);ka(a,c.groupText);a=k("div","split");Wb.appendChild(a);vc=k("input",null,"text");a.appendChild(vc);0<c.maximumEventLocationLength&&(vc.maxLength=c.maximumEventLocationLength);wc=k("input",null,"text");a.appendChild(wc);0<c.maximumEventGroupLength&&(wc.maxLength=c.maximumEventGroupLength);
+ka(Wb,c.descriptionText);xc=k("textarea","custom-scroll-bars");Wb.appendChild(xc);0<c.maximumEventDescriptionLength&&(xc.maxLength=c.maximumEventDescriptionLength);ka(Wb,c.urlText);od=k("input",null,"url");Wb.appendChild(od)}function Kc(){U(null,J)}function Xb(){zf.disabled=Eb.checked;ke.disabled=!ob.checked;uc.disabled=!ob.checked;ie.disabled=!ob.checked;je.disabled=!ob.checked;Yb.disabled=!ob.checked}function ii(a){a=r(a)?a:fa;var b=!1;(a=r(a)&&F(a.locked)?a.locked:!1)?b=!0:nd.checked&&(Ia.value=
+lb.value,mb.value="00:00",nb.value="23:59",b=!0);Ia.disabled=b;mb.disabled=b;nb.disabled=b;var d=Kd(lb),e=Kd(Ia);Fh(Ia,d);Fh(dc,e);d>e&&Zc(d,Ia);a||(e>d||e<d?(b=!0,Eb.checked=!0):b=!1);Eb.disabled=b;de.disabled=b;ee.disabled=b;fe.disabled=b;ge.disabled=b;he.disabled=b;ob.disabled=b;zf.disabled=b;Yb.disabled=b;uc.disabled=b;ie.disabled=b;je.disabled=b;ke.disabled=b;a||Xb()}function U(a,b,d){vb(B.body,Aa);var e=Qa,f=void 0;f=r(f)?f:0;e=e.getElementsByClassName("tab");0<e.length&&e[f].click();xf.innerHTML=
+n.empty;f=k("div","radio-buttons-container");xf.appendChild(f);for(var g in K)K.hasOwnProperty(g)&&(K[g].eventEditorInput=S(f,K[g].text,"Type"));r(a)?(y(Vb,c.editEventTitle),Ah(a.type),ae.value=c.updateText,vf.style.display="inline-block",fa=a,mb.value=af(a.from),nb.value=af(a.to),nd.checked=ki(a.isAllDay),ce.checked=ki(a.showAlerts,!0),kb.value=C(a.title),xc.value=C(a.description),vc.value=C(a.location),wc.value=C(a.group),od.value=C(a.url),Gc.value=C(a.color,c.defaultEventBackgroundColor),Hc.value=
+C(a.colorText,c.defaultEventTextColor),Ic.value=C(a.colorBorder,c.defaultEventBorderColor),Yb.value=I(a.repeatEveryCustomValue,1),Zc(a.from,lb),Zc(a.to,Ia),d=I(a.repeatEvery),d===x.never?Eb.checked=!0:d===x.everyDay?de.checked=!0:d===x.everyWeek?ee.checked=!0:d===x.every2Weeks?fe.checked=!0:d===x.everyMonth?ge.checked=!0:d===x.everyYear?he.checked=!0:d===x.custom&&(ob.checked=!0),d=I(a.repeatEveryCustomType),d===Q.daily?uc.checked=!0:d===Q.weekly?ie.checked=!0:d===Q.monthly?je.checked=!0:d===Q.yearly&&
+(ke.checked=!0),d=Rb(a.repeatEveryExcludeDays),Ae.checked=-1<d.indexOf(1),Be.checked=-1<d.indexOf(2),Ce.checked=-1<d.indexOf(3),De.checked=-1<d.indexOf(4),Ee.checked=-1<d.indexOf(5),Fe.checked=-1<d.indexOf(6),Ge.checked=-1<d.indexOf(0),Zc(a.repeatEnds,dc)):(g=new Date,b=r(b)?b:g,Xc(b)&&(b.setHours(g.getHours()),b.setMinutes(g.getMinutes())),g=$c(b,c.defaultEventDuration),y(Vb,c.addEventTitle),Ah(),ae.value=c.addText,vf.style.display="none",fa={},nd.checked=!1,ce.checked=!0,kb.value=n.empty,xc.value=
+n.empty,vc.value=n.empty,wc.value=n.empty,od.value=n.empty,Gc.value=c.defaultEventBackgroundColor,Hc.value=c.defaultEventTextColor,Ic.value=c.defaultEventBorderColor,Eb.checked=!0,Ae.checked=!1,Be.checked=!1,Ce.checked=!1,De.checked=!1,Ee.checked=!1,Fe.checked=!1,Ge.checked=!1,dc.value=null,Yb.value="1",uc.checked=!0,za(d)&&(b.setHours(d[0]),b.setMinutes(d[1]),g.setHours(d[0]),g.setMinutes(d[1]),g=$c(g,c.defaultEventDuration)),mb.value=af(b),nb.value=af(g),Zc(b,lb),Zc(g,Ia));w(Vb,"ib-close",c.closeTooltipText,
+id,!0);a=rf(a);for(var h in K)K.hasOwnProperty(h)&&r(K[h].eventEditorInput)&&(K[h].eventEditorInput.disabled=a);ae.disabled=a;lb.disabled=a;Ia.disabled=a;mb.disabled=a;nb.disabled=a;nd.disabled=a;ce.disabled=a;kb.disabled=a;ji.disabled=a;xc.disabled=a;vc.disabled=a;wc.disabled=a;od.disabled=a;Eb.disabled=a;de.disabled=a;ee.disabled=a;fe.disabled=a;ge.disabled=a;he.disabled=a;ob.disabled=a;zf.disabled=a;ii();La.push(id);Qa.style.display="block";kb.focus()}function pc(){kb.focus();kb.select()}function Nj(){var a=
+mb.value.split(":"),b=nb.value.split(":"),d=yc(kb.value),e=yc(od.value);if(2>a.length)le(c.fromTimeErrorMessage);else if(2>b.length)le(c.toTimeErrorMessage);else if(d===n.empty)le(c.titleErrorMessage);else if(0<e.length&&!li(e))le(c.urlErrorMessage);else{a=Kd(lb);b=Kd(Ia);var f=yc(xc.value),g=yc(vc.value),h=yc(wc.value),l=Kd(dc,null),p=parseInt(Yb.value);var q=0;for(var v in K)if(K.hasOwnProperty(v)&&r(K[v].eventEditorInput)&&K[v].eventEditorInput.checked){q=parseInt(v);break}bf(a,mb.value);bf(b,
+nb.value);isNaN(p)&&(p=0,Eb.checked=!0,uc.checked=!0);b<a?le(c.toSmallerThanFromErrorMessage):(id(),v=r(fa.id),d={from:a,to:b,title:d,description:f,location:g,group:h,isAllDay:nd.checked,showAlerts:ce.checked,color:fa.color,colorText:fa.colorText,colorBorder:fa.colorBorder,repeatEveryExcludeDays:fa.repeatEveryExcludeDays,repeatEnds:l,url:e,repeatEveryCustomValue:p,type:q},Eb.checked?d.repeatEvery=x.never:de.checked?d.repeatEvery=x.everyDay:ee.checked?d.repeatEvery=x.everyWeek:fe.checked?d.repeatEvery=
+x.every2Weeks:ge.checked?d.repeatEvery=x.everyMonth:he.checked?d.repeatEvery=x.everyYear:ob.checked&&(d.repeatEvery=x.custom),uc.checked?d.repeatEveryCustomType=Q.daily:ie.checked?d.repeatEveryCustomType=Q.weekly:je.checked?d.repeatEveryCustomType=Q.monthly:ke.checked&&(d.repeatEveryCustomType=Q.yearly),v?d.id=fa.id:(d.organizerName=c.organizerName,d.organizerEmailAddress=c.organizerEmailAddress),v?qa.updateEvent(fa.id,d,!1):qa.addEvent(d,!1),ra(),la())}}function id(a){me(a);Ka(B.body,Aa);Qa.style.display=
+"none"}function Mj(){Af();Je(c.confirmEventRemoveTitle,c.confirmEventRemoveMessage,function(){ne();id();r(fa.id)&&(qa.removeEvent(fa.id,!0),la())},function(){ne()})}function la(){W(ma)&&Sb(J);W(oa)&&rh();W(wa)&&Nc(Bb)}function oc(a,b,d,e){d=r(d)?d:"09:00";e=r(e)?d:"09:00";bf(a,d);bf(b,e);b=$c(b,c.defaultEventDuration);a={from:a,to:b,title:c.newEventDefaultTitle,description:n.empty,location:n.empty,group:n.empty,isAllDay:!1,showAlerts:!0,color:c.defaultEventBackgroundColor,colorText:c.defaultEventTextColor,
+colorBorder:c.defaultEventBorderColor,repeatEveryExcludeDays:[],repeatEnds:null,url:n.empty,repeatEveryCustomValue:n.empty,repeatEvery:x.never,repeatEveryCustomType:Q.daily,organizerName:n.empty,organizerEmailAddress:n.empty,type:0,locked:!1};qa.addEvent(a,!1);ra();la();return a}function rf(a){return r(a)&&F(a.locked)?a.locked:!1}function le(a){Je(c.errorText,a,ne,null,!1,!1);Af()}function Af(){wf.style.display="block"}function ne(){wf.style.display="none"}function Ti(){ye();fa.color=Gc.value;fa.colorText=
+Hc.value;fa.colorBorder=Ic.value}function ye(a){me(a);ne();Kb.style.display="none"}function Oj(){La.push(ye);Kb.style.display="block";Af()}function Ui(){ze();var a=[];Ae.checked&&a.push(1);Be.checked&&a.push(2);Ce.checked&&a.push(3);De.checked&&a.push(4);Ee.checked&&a.push(5);Fe.checked&&a.push(6);Ge.checked&&a.push(0);fa.repeatEveryExcludeDays=a}function ze(a){me(a);ne();Lb.style.display="none"}function Pj(){La.push(ze);Lb.style.display="block";Af()}function Je(a,b,d,e,f,g){f=r(f)?f:!1;g=r(g)?g:
+!0;La.push(!1);ec.style.display="block";y(Zf,a);y($f,b);fc.onclick=mi;fc.addEventListener("click",d);gc.onclick=mi;g?(gc.style.display="inline-block",fc.value=c.yesText):(gc.style.display="none",fc.value=c.okText);f?(ag.style.display="block",He.checked=!1):(ag.style.display="none",He.checked=!0);qf(e)&&gc.addEventListener("click",e)}function mi(){La.pop();ec.style.display="none"}function Lc(a){vb(B.body,Aa);La.push(Ie);Mb.style.display="block";Fb=a;bg.checked=!0;Fa.value=n.empty;Fa.focus()}function Ie(a){me(a);
+Ka(B.body,Aa);Mb.style.display="none"}function Vi(){Ie();bg.checked?Gb(Fb,"csv",Fa.value):hh.checked?Gb(Fb,"xml",Fa.value):ih.checked?Gb(Fb,"json",Fa.value):jh.checked?Gb(Fb,"text",Fa.value):kh.checked?Gb(Fb,"ical",Fa.value):lh.checked?Gb(Fb,"md",Fa.value):mh.checked?Gb(Fb,"html",Fa.value):nh.checked&&Gb(Fb,"tsv",Fa.value)}function Wi(){if(!t&&null===ha){ha=k("div","calendar-dialog search");B.body.appendChild(ha);var a=k("div","title-bar");y(a,c.searchEventsTitle);ha.appendChild(a);Fc(a,ha,function(){Ng=
+!0;oe()});a.ondblclick=Og;var b=w(a,"ib-close",c.closeTooltipText,Id);b.onmousedown=D;b.onmouseup=D;zc=w(a,"ib-minus",c.minimizedTooltipText,Og);zc.onmousedown=D;zc.onmouseup=D;pb=k("div","contents");ha.appendChild(pb);a=k("div","history-container");pb.appendChild(a);ba=k("input",null,"text");ba.placeholder=c.searchTextBoxPlaceholder;ba.oninput=pd;ba.onpropertychange=pd;ba.onkeypress=Qj;a.appendChild(ba);qb=k("div","ib-arrow-down-full");qb.style.display="none";qb.onclick=Rj;a.appendChild(qb);$a=k("div",
+"history-dropdown custom-scroll-bars");a.appendChild($a);a=k("div","checkbox-container");pb.appendChild(a);Bf=M(a,c.notSearchText,rb)[0];Cf=M(a,c.matchCaseText,rb)[0];qd=M(a,c.advancedText+":",Sj)[0];qd.checked=!0;pe=k("div","advanced");pb.appendChild(pe);var d=k("div","split");pe.appendChild(d);b=k("div","split-contents");d.appendChild(b);a=k("div","split-contents");d.appendChild(a);ka(b,c.includeText,"text-header");d=k("div","checkbox-container");b.appendChild(d);qe=M(d,c.titleText.replace(":",
+n.empty),rb)[0];Df=M(d,c.locationText.replace(":",n.empty),rb)[0];Ef=M(d,c.descriptionText.replace(":",n.empty),rb)[0];Ff=M(d,c.groupText.replace(":",n.empty),rb)[0];Gf=M(d,c.urlText.replace(":",n.empty),rb)[0];qe.checked=!0;ka(a,c.optionsText,"text-header");b=k("div","radio-buttons-container");a.appendChild(b);Hf=S(b,c.startsWithText,"SearchOptionType",rb);If=S(b,c.endsWithText,"SearchOptionType",rb);Jf=S(b,c.containsText,"SearchOptionType",rb);Jf.checked=!0;a=k("div","buttons-container");pb.appendChild(a);
+Kf=sa(a,c.previousText,"previous",ni);Lf=sa(a,c.nextText,"next",oi)}}function Sj(){pe.style.display=qd.checked?"block":"none";tg();oe()}function rb(){oe();pd(!1)}function pd(a){a=r(a)?a:!0;0<ab.length&&Jh(G," focused-event");Kf.disabled=!0;Lf.disabled=ba.value===n.empty;ab=[];Zb=0;Pg=null;a&&pi();oe()}function Ed(){"block"!==ha.style.display&&(ab=[],ha.style.display="block",pd(!1),ba.value=u.lastSearchText,Bf.checked=u.not,Cf.checked=u.matchCase,qd.checked=u.showAdvanced,qe.checked=u.searchTitle,
+Df.checked=u.searchLocation,Ef.checked=u.searchDescription,Ff.checked=u.searchGroup,Gf.checked=u.searchUrl,Hf.checked=u.startsWith,If.checked=u.endsWith,Jf.checked=u.contains,pe.style.display=qd.checked?"block":"none",tg());"block"!==pb.style.display&&Og();ba.focus();ba.select();0<u.history.length&&(qb.style.display="block")}function tg(){Ng||t||(xa(u.left)?ha.style.left=u.left+"px":ha.style.left=Ga.innerWidth/2-ha.offsetWidth/2+"px",xa(u.top)?ha.style.top=u.top+"px":ha.style.top=Ga.innerHeight/2-
+ha.offsetHeight/2+"px")}function Id(){var a=!1;"block"===ha.style.display&&(ha.style.display="none",pd(),a=!0);return a}function Og(){"block"===pb.style.display?(pb.style.display="none",zc.className="ib-square-hollow",ua(zc,c.restoreTooltipText)):(pb.style.display="block",zc.className="ib-minus",ua(zc,c.minimizedTooltipText))}function ni(){if(0<Zb){Zb--;var a=ab[Zb];qi();Z(a.from);ri(a)}}function Qj(a){a.keyCode===P.enter&&aa(a)&&!Kf.disabled?ni():a.keyCode!==P.enter||Lf.disabled?pi():oi()}function oi(){if(0===
+ab.length){var a="day-",b=Bf.checked,d=Cf.checked,e=d?ba.value:ba.value.toLowerCase(),f={},g=Nb(Jc()),h=g.length,l=W(ma),p=W(oa),q=W(wa);l?a="full-day-":p?a="month-":q&&(a="week-day-");oe(!0);for(var v=0;v<h;v++){var E=g[v];if(ad(E)){var ea=C(E.title),Tb=C(E.location),Ub=C(E.description),Qg=C(E.group),Rg=C(E.url),$b=!1;d||(ea=ea.toLowerCase(),Ub=Ub.toLowerCase(),Tb=Tb.toLowerCase(),Qg=Qg.toLowerCase(),Rg=Rg.toLowerCase());qe.checked&&re(ea,e)?$b=!0:Df.checked&&re(Tb,e)?$b=!0:Ef.checked&&re(Ub,e)?
+$b=!0:Ff.checked&&re(Qg,e)?$b=!0:Gf.checked&&re(Rg,e)&&($b=!0);b&&($b=!$b);!$b||null===tb(a+E.id)&&(l||p||q)||(l||p||q?ab.push(hd(E,!1)):(ea=E.from.getMonth()+"-"+E.from.getFullYear(),f.hasOwnProperty(ea)||(ab.push(hd(E,!1)),f[ea]=!0)))}}}else Zb++;qi();0<ab.length&&(a=ab[Zb],Z(new Date(a.from)),ri(a))}function ri(a){var b="day-",d=W(ma),e=W(oa),f=W(wa);Jh(G," focused-event");d?b="full-day-":e?b="month-":f&&(b="week-day-");b=tb(b+a.id);null!==b&&(b.className+=" focused-event",Pg=a.id,(d||e||f)&&b.scrollIntoView())}
+function qi(){Kf.disabled=0===Zb;Lf.disabled=Zb===ab.length-1||0===ab.length}function re(a,b){return Hf.checked?a.substring(0,b.length)===b:If.checked?a.substring(a.length-b.length,a.length)===b:-1<a.indexOf(b)}function oe(a){a=r(a)?a:!1;mc(Ca.searchOptionsChanged);var b=yc(ba.value);a&&(qb.style.display="block");Uc(Ca.searchOptionsChanged,function(){var d=!0,e=u.history.length;if(a){d=!1;for(var f=0;f<e;f++)if(u.history[f]===b){d=!0;break}d||u.history.push(b)}if(!a||d)u.lastSearchText=b,u.not=Bf.checked,
+u.matchCase=Cf.checked,u.showAdvanced=qd.checked,u.searchTitle=qe.checked,u.searchLocation=Df.checked,u.searchDescription=Ef.checked,u.searchGroup=Ff.checked,u.searchUrl=Gf.checked,u.startsWith=Hf.checked,u.endsWith=If.checked,u.contains=Jf.checked,Ng&&(u.left=ha.offsetLeft,u.top=ha.offsetTop),z("onSearchOptionsUpdated",u)},2E3,!1)}function pi(){var a=u.history.length;0<a?(qb.style.display="block",mc(Ca.searchEventsHistoryDropDown),Uc(Ca.searchEventsHistoryDropDown,function(){var b=ba.value,d=!1;
+if(yc(b)!==n.empty){si();$a.innerHTML=n.empty;for(var e=0;e<a;e++){var f=u.history[e],g=b;f.substring(0,g.length).toLowerCase()===g.toLowerCase()&&f.toLowerCase()!==b.toLowerCase()&&(ti(u.history[e],b.length),d=!0)}}d?ui():Xe()},150,!1)):qb.style.display="none"}function si(){u.history.sort(function(a,b){var d=0,e=a.toLowerCase(),f=b.toLowerCase();e<f?d=-1:e>f&&(d=1);return d})}function Rj(a){D(a);if("block"!==$a.style.display){si();a=u.history.length;$a.innerHTML=n.empty;ba.focus();for(var b=0;b<
+a;b++)ti(u.history[b],0);ui()}else Xe()}function ti(a,b){var d=k("div","history-dropdown-item");$a.appendChild(d);var e=k("span","search-search");y(e,a.substring(0,b));d.appendChild(e);e=k("span");y(e,a.substring(b));d.appendChild(e);d.onclick=function(f){D(f);Xe();ba.value=a;ba.selectionStart=ba.selectionEnd=ba.value.length;ba.focus();pd(!1)}}function Xe(){null!==$a&&($a.style.display="none",qb.className="ib-arrow-down-full")}function ui(){null!==$a&&($a.style.display="block",qb.className="ib-arrow-up-full")}
+function Xi(){if(!t&&null===fb){fb=k("div","calendar-dialog configuration");B.body.appendChild(fb);var a=k("div","title-bar");y(a,c.configurationTitleText);fb.appendChild(a);Fc(a,fb,null);w(a,"ib-close",c.closeTooltipText,vi,!0);a=k("div","contents");fb.appendChild(a);var b=hi(a);kd(b,c.displayTabText,function(d){ld(d,Hb,fb)},!0);kd(b,c.organizerTabText,function(d){ld(d,rd,fb)});Hb=md(a,!0,!1);rd=md(a,!1,!1);fg=M(Hb,c.enableAutoRefreshForEventsText)[0];gg=M(Hb,c.enableBrowserNotificationsText,null,
+null,null,"checkbox-tabbed-in")[0];hg=M(Hb,c.enableTooltipsText,null,null,null,"checkbox-tabbed-down")[0];ig=M(Hb,c.enableDragAndDropForEventText)[0];jg=M(Hb,c.enableDayNameHeadersInMainDisplayText)[0];kg=M(Hb,c.showEmptyDaysInWeekViewText)[0];lg=M(Hb,c.showHolidaysInTheDisplaysText)[0];ka(rd,c.organizerNameText);Re=k("input",null,"text");rd.appendChild(Re);ka(rd,c.organizerEmailAddressText);Se=k("input",null,"email");rd.appendChild(Se);b=k("div","buttons-container");a.appendChild(b);sa(b,c.updateText,
+"update",Tj);sa(b,c.cancelText,"cancel",vi)}}function Tj(){fg.checked?qa.startTheAutoRefreshTimer():qa.stopTheAutoRefreshTimer();c.eventNotificationsEnabled=gg.checked;c.tooltipsEnabled=hg.checked;c.dragAndDropForEventsEnabled=ig.checked;c.showDayNamesInMainDisplay=jg.checked;c.showEmptyDaysInWeekView=kg.checked;c.showHolidays=lg.checked;c.organizerName=Re.value;c.organizerEmailAddress=Se.value;db=!1;z("onOptionsUpdated",c);Sg();mg();Z(A,!0,!0)}function vi(){mg()}function mg(a){me(a);Ka(B.body,Aa);
+fb.style.display="none"}function ad(a){var b=C(a.group),d=b.toLowerCase();a=I(a.type);var e=!0;b!==n.empty?r(L.visibleGroups)&&(e=-1<L.visibleGroups.indexOf(d)):e=!c.hideEventsWithoutGroupAssigned;e&&r(L.visibleEventTypes)&&K.hasOwnProperty(a)&&(e=-1<L.visibleEventTypes.indexOf(a));return e}function Yi(){null===O&&(O=k("div","calendar-tooltip"),B.body.appendChild(O),Tg=k("div","ib-close"),Mf=k("div","ib-plus"),Nf=k("div","title-buttons"),Nf.appendChild(Tg),Nf.appendChild(Mf),Ac=k("div","title"),se=
+k("div","date"),Bc=k("div","duration"),te=k("div","repeats"),ue=k("div","description"),ve=k("div","location"),Cc=k("div","url"),Tg.onclick=Tc,Mf.onclick=function(){U(Md)},document.body.addEventListener("mousemove",Tc))}function Ih(a,b,d,e){D(a);mc(Ca.showToolTip);Tc();e=r(e)?e:!1;"block"!==O.style.display&&c.tooltipsEnabled&&Uc(Ca.showToolTip,function(){if(e||!B.body.contains(Aa)&&!Ve()&&!gi()&&null===Y){d=r(d)?d:n.empty;O.className=d===n.empty?"calendar-tooltip-event":"calendar-tooltip";if(d!==n.empty)y(O,
+d);else{O.onmousemove=D;Md=b;O.innerHTML=n.empty;Ac.innerHTML=n.empty;Bc.innerHTML=n.empty;O.appendChild(Nf);O.appendChild(Ac);O.appendChild(se);O.appendChild(Bc);jb(Mf,c.manualEditingEnabled);Oa(b.url)?(y(Cc,Uj(b.url)),vb(O,Cc),Cc.onclick=function(g){D(g);Mg(b.url);Tc()}):(Cc.innerHTML=n.empty,Cc.onclick=null,Ka(O,Cc));if(I(b.repeatEvery)>x.never){var f=k("div","ib-refresh-medium ib-no-hover ib-no-active");f.style.borderColor=Ac.style.color;Ac.appendChild(f)}Ac.innerHTML+=bd(b.title);xa(b.repeatEvery)&&
+b.repeatEvery>x.never?(y(te,c.repeatsText.replace(":",n.empty)+n.space+cd(b.repeatEvery)),vb(O,te)):(te.innerHTML=n.empty,Ka(O,te));Oa(b.location)?(y(ve,b.location),vb(O,ve)):(ve.innerHTML=n.empty,Ka(O,ve));Oa(b.description)?(y(ue,b.description),vb(O,ue)):(ue.innerHTML=n.empty,Ka(O,ue));b.from.getDate()===b.to.getDate()?b.isAllDay?y(se,c.allDayText):(y(se,Ld(b.from,b.to)),y(Bc,Pb(b.from,b.to))):(kf(se,b.from,b.to),y(Bc,Pb(b.from,b.to)));Bc.innerHTML===n.empty&&O.removeChild(Bc)}Od(a,O)}},c.eventTooltipDelay,
+!1)}function Tc(){mc(Ca.showToolTip);wi()&&(O.style.display="none",Md=null,O.onmousemove=null)}function wi(){return Ug(Ca.showToolTip)||null!==O&&"block"===O.style.display}function ua(a,b,d){null!==a&&(a.onmousemove=function(e){Ih(e,null,b,d)})}function Fc(a,b,d){a.onmousedown=function(e){Dc||(da(),bb=b,Dc=!0,xi=e.pageX-bb.offsetLeft,yi=e.pageY-bb.offsetTop,Of=bb.offsetLeft,Pf=bb.offsetTop)};a.onmouseup=function(){zi(d)};a.oncontextmenu=function(){zi(null)}}function zi(a){Dc&&(Dc=!1,bb=null,Pf=Of=
+0,null!==a&&a())}function ij(a){Dc&&(bb.style.left=a.pageX-xi+"px",bb.style.top=a.pageY-yi+"px")}function jj(){Dc&&(bb.style.left=Of+"px",bb.style.top=Pf+"px",Dc=!1,bb=null,Pf=Of=0)}function hi(a){var b=k("div");a.appendChild(b);return b}function kd(a,b,d,e){e=r(e)?e:!1;var f=k("div","tab tab-control"+(e?"-selected":n.empty));y(f,b);a.appendChild(f);f.onclick=function(){d(f)}}function md(a,b,d){b=r(b)?b:!1;d=r(d)?d:!0;var e=k("div","checkbox-container tab-content custom-scroll-bars");a.appendChild(e);
+d&&(e.className+=" custom-scroll-bars");b||(e.style.display="none");return e}function ld(a,b,d){var e=d.getElementsByClassName("tab-control-selected"),f=e.length;d=d.getElementsByClassName("tab-content");for(var g=d.length,h=0;h<f;h++)e[h].className="tab tab-control";for(e=0;e<g;e++)d[e].style.display="none";a.className="tab tab-control-selected";b.style.display="block"}function df(a,b,d,e){e=r(e)?e:!1;r(d)&&d<new Date&&(a.className+=" expired");e&&r(d)&&(d.getFullYear()!==A.getFullYear()||d.getMonth()!==
+A.getMonth())&&(a.className+=" not-in-current-month");Oa(b.color)?(a.style.backgroundColor=b.color,Oa(b.colorText)&&(a.style.color=b.colorText),Oa(b.colorBorder)&&(a.style.borderColor=b.colorBorder)):b.isAllDay&&(a.className+=" all-day")}function ef(a,b){Pg===b.id&&(a.className+=" focused-event");Ai(b.id)&&(a.className+=" selected-event");for(var d=!1,e=X.length,f=0;f<e;f++)if(X[f].id===b.id){d=!0;break}d&&(a.className=hb?a.className+" cut-event":a.className+" copy-event")}function we(a,b,d){d=r(d)?
+d:!1;var e=B.getElementsByClassName("event");e=[].slice.call(e);for(var f=e.length,g=0;g<f;g++){var h=e[g],l=h.getAttribute("event-id");null!==l&&l===a.toString()&&(h.className=d?h.className.replace(n.space+b,n.empty):h.className+(n.space+b))}}function oj(a,b){Bi(function(){if(Xc(a)&&!Ci.hasOwnProperty(b.id)&&(!F(b.showAlerts)||b.showAlerts)){var d=new Date,e=new Date,f=new Date,g=I(b.repeatEvery);d.setHours(b.from.getHours(),b.from.getMinutes(),0,0);e.setHours(b.to.getHours(),b.to.getMinutes(),0,
+0);g!==x.never||Xc(b.from)||d.setHours(0,0,0,0);g!==x.never||Xc(b.to)||e.setHours(23,59,59,99);f>=d&&f<=e&&Vj(b)}},!1,b)}function Vj(a){Ci[a.id]=!0;(new Notification(c.eventNotificationTitle,{body:c.eventNotificationBody.replace("{0}",a.title)})).onclick=function(){var b=C(a.url);b===n.empty?U(a):Mg(b);z("onNotificationClicked",a)}}function Sg(){Bi(function(){"granted"!==Notification.permission&&Notification.requestPermission()})}function Bi(a,b,d){c.eventNotificationsEnabled&&!t&&(b=r(b)?b:!0,Notification?
+a():b&&console.error("Browser notifications API unavailable."),r(d)&&z("onNotification",d))}function Mg(a){Ga.open(a,c.urlWindowTarget);sb("onEventUrlClicked",a)}function Wc(a){a=r(a)?a:!0;for(var b=X.length,d=0;d<b;d++){var e=X[d];hb?we(e.id,"cut-event",a):we(e.id,"copy-event",a)}}function ai(a){X=[];var b=ya.length;if(0<b)for(a=0;a<b;a++)X.push(ya[a]);else X.push(a)}function Ai(a){for(var b=!1,d=ya.length,e=0;e<d;e++)if(ya[e].id===a){b=!0;break}return b}function ug(a,b){for(var d=X.length,e=0;e<
+d;e++){var f=X[e],g=Ob(f.from,f.to);f=b?f:hd(f);f.from.setDate(a.getDate());f.from.setMonth(a.getMonth());f.from.setFullYear(a.getFullYear());f.to.setDate(a.getDate());f.to.setMonth(a.getMonth());f.to.setFullYear(a.getFullYear());f.to.setDate(f.to.getDate()+g);b?z("onEventUpdated",f):(f.id=null,qa.addEvent(f,!1,!0))}b&&(xb(),X=[],hb=!1);ra();la()}function gf(a,b){D(a);da();if(rf(b))aa(a)||xb();else if(aa(a))if(Ai(b.id)){for(var d=ya.length,e=0;e<d;e++)if(ya[e].id===b.id){ya.splice(e,1);break}we(b.id,
+"selected-event",!0)}else ya.push(b),we(b.id,"selected-event",!1);else xb()}function xb(){for(var a=ya.length,b=0;b<a;b++)we(ya[b].id,"selected-event",!0);ya=[]}function Eh(a){X=[];hb=r(a)?a:!1;a=ya.length;if(0<a){for(var b=0;b<a;b++)X.push(ya[b]);Wc(!1)}}function Hh(){0<c.autoRefreshTimerDelay&&!t&&Gd&&Uc(Ca.autoRefresh,function(){ta()},c.autoRefreshTimerDelay)}function Gh(){0<c.autoRefreshTimerDelay&&!t&&Gd&&mc(Ca.autoRefresh)}function ta(a,b){a=r(a)?a:!1;b=r(b)?b:!1;if(Ch()||a)la(),Ph(),ah?Z():
+ra(),b&&sb("onRefresh")}function Ch(){return!wi()&&!B.body.contains(Aa)&&!Ve()&&!gi()&&!wd()&&null===Y}function Uc(a,b,d,e){e=r(e)?e:!0;Ug(a)||(cb[a]=e?setInterval(b,d):setTimeout(function(){b();delete cb[a]},d))}function mc(a){Ug(a)&&(clearTimeout(cb[a]),delete cb[a])}function Ug(a){return cb.hasOwnProperty(a)&&null!==cb[a]}function zh(){var a=[],b=[];Sa(function(d){d=C(d.group);d!==n.empty&&-1===b.indexOf(d.toLowerCase())&&(a.push(d),b.push(d.toLowerCase()))});a.sort();return a}function Eg(a){W(a)||
+(a.className+=" overlay-shown",Id())}function nc(a){W(a)&&(a.className=a.className.replace(" overlay-shown",n.empty))}function W(a){return null!==a&&-1<a.className.indexOf("overlay-shown")}function k(a,b,d){a=a.toLowerCase();var e="text"===a;Vg.hasOwnProperty(a)||(Vg[a]=e?B.createTextNode(n.empty):B.createElement(a));a=Vg[a].cloneNode(!1);r(b)&&(a.className=b);r(d)&&(a.type=d);return a}function ka(a,b,d){var e=k("p");y(e,b);a.appendChild(e);r(d)&&(e.className=d);return e}function R(a,b,d,e,f,g){f=
+r(f)?f:!1;(g=r(g)?g:!1)&&a.appendChild(k("div","separator"));d=k("span",d);g=qf(e);y(d,b);a.appendChild(d);g&&(d.onclick=e);f&&g&&(d.ondblclick=D)}function sa(a,b,d,e,f){d=k("input",d,"button");d.value=b;d.onclick=e;a.appendChild(d);r(f)&&ua(d,f,!0);return d}function tb(a){sd.hasOwnProperty(a)&&null!==sd[a]||(sd[a]=Di(a));B.body.contains(sd[a])||(sd[a]=Di(a));return sd[a]}function Di(a){var b=null;if(null===G)b=B.getElementById(a);else for(var d=G.getElementsByTagName("*"),e=d.length,f=0;f<e;f++)if(d[f].id===
+a){b=d[f];break}return b}function vb(a,b){try{a.contains(b)||a.appendChild(b)}catch(d){console.warn(d.message)}}function Ka(a,b){try{a.contains(b)&&a.removeChild(b)}catch(d){console.warn(d.message)}}function D(a){a.preventDefault();a.cancelBubble=!0}function Od(a,b){var d=a.pageX,e=a.pageY;var f=B.documentElement;var g=(Ga.pageXOffset||f.scrollLeft)-(f.clientLeft||0);f=(Ga.pageYOffset||f.scrollTop)-(f.clientTop||0);b.style.display="block";d+b.offsetWidth>Ga.innerWidth?d-=b.offsetWidth:d++;e+b.offsetHeight>
+Ga.innerHeight?e-=b.offsetHeight:e++;d<g&&(d=a.pageX+1);e<f&&(e=a.pageY+1);b.style.left=d+"px";b.style.top=e+"px"}function ub(a,b){try{a.type=b}catch(d){console.error(d.message),a.type="text"}}function Fg(a){for(var b=0,d=0;a&&!isNaN(a.offsetLeft)&&!isNaN(a.offsetTop);)b+=a.offsetLeft-a.scrollLeft,d+=a.offsetTop-a.scrollTop,a=a.offsetParent;return{left:b,top:d}}function me(a){(a=r(a)?a:!0)&&La.pop()}function hd(a,b){b=r(b)?b:!0;var d=JSON.parse(JSON.stringify(a));d.from=new Date(d.from);d.to=new Date(d.to);
+r(d.repeatEnds)&&(d.repeatEnds=new Date(d.repeatEnds));delete d.created;delete d.lastUpdated;b&&delete d.id;return d}function bd(a){var b=a;c.allowHtmlInDisplay||(b=k("div"),b.innerHTML=a,b=b.innerText);return b}function y(a,b){c.allowHtmlInDisplay?a.innerHTML=b:a.innerText=bd(b)}function jb(a,b){null!==a&&(a.style.display=b?"inline-block":"none")}function sf(a,b,d){a=G.getElementsByClassName(a);for(var e=a.length,f=0;f<e;f++)for(var g=a[f].children,h=g.length,l=0;l<h;l++)g[l]!==d&&b(g[l])}function S(a,
+b,d,e){var f=k("div","radio-button-container");a.appendChild(f);a=k("label","radio-button");f.appendChild(a);f=k("input",null,"radio");f.name=d;a.appendChild(f);r(e)&&(f.onchange=e);a.appendChild(k("span","check-mark"));R(a,b,"text");return f}function M(a,b,d,e,f,g){g=r(g)?n.space+g:n.empty;var h=k("div");a.appendChild(h);a=k("label","checkbox"+g);h.appendChild(a);h=k("input",null,"checkbox");a.appendChild(h);r(e)&&(h.name=e);r(d)&&(h.onchange=d);r(f)&&(h.checked=f);a.appendChild(k("span","check-mark"));
+R(a,b,"text");return[h,a]}function w(a,b,d,e,f){b=k("div",b);b.ondblclick=D;b.onclick=e;a.appendChild(b);ua(b,d,f);return b}function Vh(a,b){a.innerHTML=n.empty;var d=k("div","no-events-available-text");a.appendChild(d);ka(d,c.noEventsAvailableFullText);c.manualEditingEnabled&&(d.appendChild(k("div")),R(d,c.clickText+n.space),R(d,c.hereText,"link",b),R(d,n.space+c.toAddANewEventText))}function N(a){a=a.toString();return 1===a.length?"0"+a:a}function yc(a){return a.replace(/^\s+|\s+$/g,n.empty)}function ch(){for(var a=
+[],b=0;32>b;b++){8!==b&&12!==b&&16!==b&&20!==b||a.push("-");var d=Math.floor(16*Math.random()).toString(16);a.push(d)}return a.join(n.empty)}function Uj(a,b){var d=a;b=r(b)?b:30;a.length>b&&(d=0===b%2?b/2:(b-1)/2,d=a.substring(0,d)+"..."+a.substring(a.length-d));return d}function r(a){return void 0!==a&&null!==a&&a!==n.empty}function qf(a){return r(a)&&"function"===typeof a}function ja(a){return r(a)&&"string"===typeof a}function xa(a){return r(a)&&"number"===typeof a}function F(a){return r(a)&&"boolean"===
+typeof a}function ac(a){return r(a)&&"object"===typeof a}function za(a){return ac(a)&&a instanceof Array}function Oa(a){return ja(a)&&a!==n.empty}function ud(a){return ac(a)&&a instanceof Date}function bh(a){return ac(a)&&void 0!==a.tagName}function li(a){return/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(a)}
+function m(a,b){return ja(a)?a:b}function H(a,b){return F(a)?a:b}function Ea(a,b){return xa(a)?a:b}function Wg(a,b){return za(a)?a:b}function Xg(a,b){return ud(a)?a:b}function Gb(a,b,d){b=r(b)?b.toLowerCase():"csv";var e=n.empty,f=Nb,g=[];g=r(a)?g.concat(a):Jc();g=Nb(g);a=f(g,!1);if("csv"===b){e=a.length;f=Yg();g=f[0];var h=f[1],l=[];f=[];for(var p=0;p<h;p++)l.push(Ei(g[p]));f.push(l.join(","));for(g=0;g<e;g++){h=f;l=Zg(a[g]);p=l.length;for(var q=0;q<p;q++)l[q]=Ei(l[q]);h.push(l.join(","))}e=f.join("\n")}else if("xml"===
+b){e=[];f=a.length;e.push('<?xml version="1.0" ?>');e.push("<Events>");for(g=0;g<f;g++){h=a[g];l=Qf(h);p=l.length;e.push("<Event>");for(q=0;q<p;q++){var v=l[q];if(h.hasOwnProperty(v)&&null!==h[v]){var E=$g(v);e.push("<"+E+">"+Rf(v,h[v])+"</"+E+">")}}e.push("</Event>")}e.push("</Events>");e=e.join("\n")}else if("json"===b){e=[];f=a.length;e.push("{");e.push('"events": [');for(g=0;g<f;g++){h=a[g];l=Qf(h);p=l.length;e.push("{");for(q=0;q<p;q++)v=l[q],h.hasOwnProperty(v)&&null!==h[v]&&e.push('"'+v+'":'+
+Rf(v,h[v],!0)+",");h=e[e.length-1];e[e.length-1]=h.substring(0,h.length-1);e.push("},")}e[e.length-1]="}";e.push("]");e.push("}");e=e.join("\n")}else if("text"===b){e=[];f=a.length;for(g=0;g<f;g++){h=a[g];l=Qf(h);p=l.length;for(q=0;q<p;q++)v=l[q],h.hasOwnProperty(v)&&null!==h[v]&&e.push($g(v)+": "+Rf(v,h[v]));e.push(n.empty)}e.pop();e=e.join("\n")}else if("ical"===b){e=[];f=a.length;e.push("BEGIN:VCALENDAR");e.push("VERSION:2.0");e.push("PRODID:Calendar.js v"+qa.getVersion());e.push("CALSCALE:GREGORIAN");
+for(g=0;g<f;g++)h=a[g],e.push("BEGIN:VEVENT"),e.push("UID:"+C(h.id)),e.push("CREATED:"+Sf(h.created)),e.push("LAST-MODIFIED:"+Sf(h.lastUpdated)),e.push("ORGANIZER;CN="+C(h.organizerName)+":MAILTO:"+C(h.organizerEmailAddress)),e.push("DTSTART:"+Sf(h.from)),e.push("DTEND:"+Sf(h.to)),e.push("SUMMARY:"+C(h.title)),e.push("END:VEVENT");e.push("END:VCALENDAR");e=e.join("\n")}else if("md"===b){e=a.length;f=Yg();g=f[1];f=["| "+f[0].join(" | ")+" |"];h=[];for(l=0;l<g;l++)h.push("---");f.push("| "+h.join(" | ")+
+" |");for(g=0;g<e;g++)h=Zg(a[g]),f.push("| "+h.join(" | ")+" |");e=f.join("\n")}else if("html"===b){e=[];f=a.length;e.push("<html>");e.push("<body>");for(g=0;g<f;g++){h=a[g];l=Qf(h);p=l.length;e.push("<h3><b>"+h.id+":</b></h3>");e.push("<ul>");for(q=0;q<p;q++)v=l[q],h.hasOwnProperty(v)&&null!==h[v]&&e.push("<li><b>"+$g(v)+"</b>: "+Rf(v,h[v])+"</li>");e.push("</ul>")}e.push("</body>");e.push("</html>");e=e.join("\n")}else if("tsv"===b){e=a.length;f=Yg();g=f[0];h=f[1];l=[];f=[];for(p=0;p<h;p++)l.push(Fi(g[p]));
+f.push(l.join("\t"));for(g=0;g<e;g++){h=f;l=Zg(a[g]);p=l.length;for(q=0;q<p;q++)l[q]=Fi(l[q]);h.push(l.join("\t"))}e=f.join("\n")}e!==n.empty&&(a=k("a"),f="text",h=g=b,"text"===b?(g="plain",h="txt"):"ical"===b?(g="calendar",h="ics"):"json"===b?f="application":"md"===b?g="x-markdown":"html"===b?g="html":"tsv"===b&&(g="tab-separated-values"),g=[f,g,h],b=g[0],f=g[1],g=g[2],r(d)||(h=new Date,d=N(h.getDate())+"-"+N(h.getMonth()+1)+"-"+h.getFullYear(),h=N(h.getHours())+"-"+N(h.getMinutes()),d=c.exportStartFilename+
+d+"_"+h+"."+g),a.style.display="none",a.setAttribute("target","_blank"),a.setAttribute("href","data:"+b+"/"+f+";charset=utf-8,"+encodeURIComponent(e)),a.setAttribute("download",d),B.body.appendChild(a),a.click(),B.body.removeChild(a),sb("onEventsExported"))}function td(a){var b=c.repeatsNever;r(a)&&(b=N(a.getDate())+"/"+N(a.getMonth()+1)+"/"+a.getFullYear(),a=N(a.getHours())+":"+N(a.getMinutes()),b=b+n.space+a);return b}function C(a,b){b=r(b)?b:n.empty;return ja(a)?a:b}function I(a,b){b=r(b)?b:0;
+return xa(a)?a:b}function qh(a,b){b=r(b)?b:0;return F(a)?a?1:0:b}function ki(a,b){b=r(b)?b:!1;return F(a)?a:b}function Rb(a,b){b=r(b)?b:[];return za(a)?a:b}function cd(a){var b=c.dailyText;a=I(a);a===x.everyDay?b=c.repeatsEveryDayText:a===x.everyWeek?b=c.repeatsEveryWeekText:a===x.every2Weeks?b=c.repeatsEvery2WeeksText:a===x.everyMonth?b=c.repeatsEveryMonthText:a===x.everyYear?b=c.repeatsEveryYearText:a===x.custom&&(b=c.repeatsByCustomSettingsText);return b}function Tf(a,b){b=r(b)?b:!1;var d=Rb(a);
+if(b)for(var e=d.length,f=0;f<e;f++)d[f]='"'+d[f]+'"';return d.join(",")}function $g(a){return a.charAt(0).toUpperCase()+a.slice(1)}function Rf(a,b,d){var e=(d=r(d)?d:!1)?'"'+C(b)+'"':C(b);"boolean"===typeof b?e=d?b.toString():b?c.yesText:c.noText:"object"===typeof b&&b instanceof Date?e=d?'"'+b.toISOString()+'"':td(b):"object"===typeof b&&b instanceof Array?e="repeatEveryExcludeDays"!==a||d?d?"["+Tf(b,!0)+"]":Tf(b):Gi(b):"number"===typeof b&&("repeatEvery"!==a||d?"repeatEveryCustomType"!==a||d?e=
+"type"!==a||d?b.toString():K[parseInt(b)].text:(a=c.dailyText,b=I(b),b===Q.daily?a=c.dailyText:b===Q.weekly?a=c.weeklyText:b===Q.monthly?a=c.monthlyText:b===Q.yearly&&(a=c.yearlyText),e=a):e=cd(b));return e}function Sf(a){var b=[];b.push(a.getFullYear());b.push(N(a.getMonth()+1));b.push(N(a.getDate()));b.push("T");b.push(N(a.getHours()));b.push(N(a.getMinutes()));b.push("00Z");return b.join(n.empty)}function Gi(a){var b=[];if(za(a))for(var d=a.length,e=0;e<d;e++){var f=a[e]-1;-1===f&&(f=6);b.push(c.dayNames[f])}return Tf(b)}
+function Yg(){var a=[c.idText,c.typeText,c.fromText,c.toText,c.isAllDayText,c.titleText,c.descriptionText,c.locationText,c.backgroundColorText,c.textColorText,c.borderColorText,c.repeatsText,c.repeatEndsText,c.repeatDaysToExcludeText,c.seriesIgnoreDatesText,c.createdText,c.lastUpdatedText,c.organizerNameText,c.organizerEmailAddressText,c.urlText,c.lockedText];return[a,a.length]}function Zg(a){var b=[];b.push(C(a.id));b.push(K[I(a.type)].text);b.push(td(a.from));b.push(td(a.to));b.push(a.isAllDay?
+c.yesText:c.noText);b.push(C(a.title));b.push(C(a.description));b.push(C(a.location));b.push(C(a.color));b.push(C(a.colorText));b.push(C(a.colorBorder));b.push(cd(a.repeatEvery));b.push(td(a.repeatEnds));b.push(Gi(a.repeatEveryExcludeDays));b.push(Tf(a.seriesIgnoreDates));b.push(td(a.created));b.push(td(a.lastUpdated));b.push(C(a.organizerName));b.push(C(a.organizerEmailAddress));b.push(C(a.url));b.push(a.locked?c.yesText:c.noText);return b}function Qf(a){var b=[],d;for(d in a)a.hasOwnProperty(d)&&
+b.push(d);b.sort();return b}function Ei(a){a=a.replace(/(\r\n|\n|\r)/gm,n.empty).replace(/(\s\s)/gm,n.space);a=a.replace(/"/g,'""');return'"'+a+'"'}function Fi(a){return a=a.replace(/\\/g,"\\\\")}function Xa(a){return qf(c[a])}function sb(a){if(null!==c&&Xa(a))c[a]()}function z(a,b){if(null!==c&&Xa(a))c[a](b)}function Dd(a){r(a)&&D(a);if(!t||Ba)a=new Date(A),a.setMonth(a.getMonth()-1),a.getFullYear()>=c.minimumYear&&(Z(a),z("onPreviousMonth",a))}function Fd(a){r(a)&&D(a);if(!t||Ba)a=new Date(A),a.setMonth(a.getMonth()+
+1),a.getFullYear()<=c.maximumYear&&(Z(a),z("onNextMonth",a))}function Le(){if(!t||Ba){var a=new Date(A);a.setFullYear(a.getFullYear()-1);a.getFullYear()>=c.minimumYear&&(Z(a),z("onPreviousYear",a))}}function Ne(){if(!t||Ba){var a=new Date(A);a.setFullYear(a.getFullYear()+1);a.getFullYear()<=c.maximumYear&&(Z(a),z("onNextYear",a))}}function Me(){if(!t||Ba){var a=new Date;if(A.getMonth()!==a.getMonth()||A.getFullYear()!==a.getFullYear())Z(),sb("onToday")}}function xe(a,b,d){K.hasOwnProperty(d)&&(ja(a)?
+K[d].text=a:K[d].text=b)}function bc(a,b){b=xa(b)?b:1;return!za(a)||a.length<b}function Uf(a,b){ac(a)||(a=ac(b)?b:{});return a}var qa=this,n={empty:"",space:" "},P={enter:13,escape:27,left:37,right:39,down:40,a:65,c:67,e:69,f:70,m:77,v:86,x:88,f5:116,f11:122},x={never:0,everyDay:1,everyWeek:2,every2Weeks:3,everyMonth:4,everyYear:5,custom:6},Q={daily:0,weekly:1,monthly:2,yearly:3},K={0:{text:"Normal Label",eventEditorInput:null},1:{text:"Meeting Label",eventEditorInput:null},2:{text:"Birthday Label",
+eventEditorInput:null},3:{text:"Holiday Label",eventEditorInput:null},4:{text:"Task Label",eventEditorInput:null}},L={visibleGroups:null,visibleEventTypes:null,visibleAllEventsMonths:{},visibleWeeklyEventsDay:{}},Ca={windowResize:"WindowResize",fullDayEventSizeTracking:"FullDayEventSizeTracking",searchOptionsChanged:"SearchOptionsChanged",searchEventsHistoryDropDown:"SearchEventsHistoryDropDown",showToolTip:"ShowToolTip",autoRefresh:"AutoRefresh"},cb={},c={},u={},cc=null,yd=null,t=!1,Ba=!1,A=null,
+Ib=null,Yf=null,Vg={},sd={},Ci={},B=null,Ga=null,ia=null,db=!1,gh=!1,Wf=!1,Ja={},Gd=!0,gd=null,Y=null,wh=null,ic=!1,ah=!1,La=[],ya=[],X=[],hb=!1,hc=[],Ad=[],Aa=null,G=null,Mc=[],Of=0,Pf=0,bb=null,Dc=!1,xi=0,yi=0,V=null,Sc=null,cg=null,Te=null,gb=null,Ue=null,eg=null,Bd=null,Ke=null,Jb=null,Ra=null,ng=!1,Oc=null,kc=null,Qc=null,jc=null,Pc=null,Rc=null,Hd=null,zd=null,Qa=null,Db=null,xf=null,yf=null,Wb=null,wf=null,Vb=null,lb=null,mb=null,Ia=null,nb=null,nd=null,ce=null,kb=null,ji=null,xc=null,vc=null,
+wc=null,od=null,Eb=null,de=null,ee=null,fe=null,ge=null,he=null,ob=null,zf=null,uc=null,ie=null,je=null,ke=null,Yb=null,fa={},ae=null,vf=null,Kb=null,Gc=null,Hc=null,Ic=null,Lb=null,Ae=null,Be=null,Ce=null,De=null,Ee=null,Fe=null,Ge=null,dc=null,ma=null,Nd=null,Ya=null,qc=null,ca=null,J=null,Ua=[],Vc=[],Kh=null,Oe=null,Lh=null,zb=null,Mh=null,hf=null,Pd=null,sc=null,Gg=null,oa=null,Th=null,Pe=null,Uh=null,dd=null,Va=[],Ye=[],wa=null,Za=null,Wh=null,Qe=null,Xh=null,ed=null,fd={},of={},Qd={},Wa=[],
+Bb=null,Ze=[],ec=null,Zf=null,$f=null,He=null,ag=null,fc=null,gc=null,Mb=null,Fa=null,bg=null,hh=null,ih=null,jh=null,kh=null,lh=null,mh=null,nh=null,Fb=null,O=null,Nf=null,Tg=null,Mf=null,Ac=null,se=null,Bc=null,te=null,ue=null,ve=null,Cc=null,Md=null,va=null,Jg=null,pf=null,tc=null,T=null,Pa=null,be=null,$d=null,Lg=null,Zd=null,Yd=null,Td=null,Xd=null,Wd=null,Vd=null,Ud=null,Sd=null,Rd=null,uf=null,tf=null,Da=null,Cg=null,Dg=null,Bg=null,jf=null,Ta=null,bi=null,di=null,ei=null,fi=null,ci=null,ha=
+null,zc=null,pb=null,ba=null,Cf=null,Bf=null,qd=null,pe=null,qe=null,Df=null,Ef=null,Ff=null,Gf=null,Hf=null,If=null,Jf=null,Kf=null,Lf=null,Ng=!1,ab=[],Zb=0,Pg=null,$a=null,qb=null,fb=null,Hb=null,rd=null,fg=null,gg=null,hg=null,ig=null,jg=null,kg=null,lg=null,Re=null,Se=null;this.turnOnFullScreen=function(){t||uh()};this.turnOffFullScreen=function(){t||th()};this.isFullScreenActivated=function(){return ic};this.startTheAutoRefreshTimer=function(){t||(Gd=!0,Hh())};this.stopTheAutoRefreshTimer=function(){t||
+(Gh(),Gd=!1)};this.destroy=function(){Wf&&fh(!1);for(var a in cb)cb.hasOwnProperty(a)&&null!==cb[a]&&(clearTimeout(cb[a]),delete cb[a]);Qb(B.body,"calendar-dialog");Qb(B.body,"calendar-drop-down-menu");Qb(B.body,"calendar-tooltip");Qb(B.body,"calendar-tooltip-event");G.className=n.empty;G.innerHTML=n.empty;sb("onDestroy",ia)};this.moveToPreviousMonth=function(){Dd()};this.moveToNextMonth=function(){Fd()};this.moveToPreviousYear=function(){Le()};this.moveToNextYear=function(){Ne()};this.moveToToday=
+function(){Me()};this.getCurrentDisplayDate=function(){return new Date(A)};this.setCurrentDisplayDate=function(a){!ud(a)||t&&!Ba||(a=new Date(a),!pa(A,a)&&a.getFullYear()>=c.minimumYear&&a.getFullYear()<=c.maximumYear&&(Z(a),z("onSetDate",a)))};this.getSelectedDatePickerDate=function(){return t?new Date(Ib):null};this.setSelectedDatePickerDate=function(a){ud(a)&&t&&(a=new Date(a),Bh(a)&&!pa(a,Ib)&&a.getFullYear()>=c.minimumYear&&a.getFullYear()<=c.maximumYear&&(a.setHours(0,0,0,0),Ec(),Vf(a),z("onDatePickerDateChanged",
+a),Ib=a))};this.exportAllEvents=function(a){c.exportEventsEnabled&&!t&&(a=ja(a)?a:"csv",Gb(null,a))};this.refresh=function(){t||ta(!0,!0)};this.setEvents=function(a,b,d){t||(d=F(d)?d:!0,Ja={},this.addEvents(a,b,!1),d&&z("onEventsSet",a))};this.setEventsFromJson=function(a,b,d){if(!t){d=F(d)?d:!0;var e=Kg(a);za(e)?this.setEvents(e,b,!1):ac(e)&&e.hasOwnProperty("events")&&this.setEvents(e.events,b,!1);d&&z("onEventsSetFromJSON",a)}};this.addEvents=function(a,b,d){if(!t){b=F(b)?b:!0;d=F(d)?d:!0;for(var e=
+a.length,f=0;f<e;f++)this.addEvent(a[f],!1,!1,!1);d&&z("onEventsAdded",a);b&&(wb(),ra(),la())}};this.addEventsFromJson=function(a,b,d){if(!t){d=F(d)?d:!0;var e=Kg(a);za(e)?this.addEvents(e,b,!1):ac(e)&&e.hasOwnProperty("events")&&this.addEvents(e.events,b,!1);d&&z("onEventsAddedFromJSON",a)}};this.addEvent=function(a,b,d,e){var f=!1;if(!t&&(e=F(e)?e:!0,ja(a.from)&&(a.from=new Date(a.from)),ja(a.to)&&(a.to=new Date(a.to)),ja(a.repeatEnds)&&(a.repeatEnds=new Date(a.repeatEnds)),ja(a.created)&&(a.created=
+new Date(a.created)),ja(a.lastUpdated)&&(a.lastUpdated=new Date(a.lastUpdated)),a.from<=a.to)){var g=a.from;g=g.getFullYear()+"-"+g.getMonth()+"-"+g.getDate();var h=ch();Ja.hasOwnProperty(g)||(Ja[g]={});if(!Ja[g].hasOwnProperty(h)){b=F(b)?b:!0;d=F(d)?d:!0;f=C(a.title);var l=C(a.description),p=C(a.location),q=C(a.group),v=C(a.url);r(L.visibleGroups)&&L.visibleGroups.push(q.toLowerCase());r(a.id)?h=a.id:a.id=h;0<c.maximumEventTitleLength&&f!==n.empty&&f.length>c.maximumEventTitleLength&&(a.title=a.title.substring(0,
+c.maximumEventTitleLength));0<c.maximumEventDescriptionLength&&l!==n.empty&&l.length>c.maximumEventDescriptionLength&&(a.description=a.description.substring(0,c.maximumEventDescriptionLength));0<c.maximumEventLocationLength&&p!==n.empty&&p.length>c.maximumEventLocationLength&&(a.location=a.location.substring(0,c.maximumEventLocationLength));0<c.maximumEventGroupLength&&q!==n.empty&&q.length>c.maximumEventGroupLength&&(a.group=a.group.substring(0,c.maximumEventGroupLength));v===n.empty||li(v)||(a.url=
+n.empty);ud(a.created)||(a.created=new Date);e&&(a.lastUpdated=new Date);Ja[g][h]=ph(a);f=!0;d&&z("onEventAdded",a);b&&(wb(),ra(),la())}}return f};this.updateEvents=function(a,b,d){if(!t){b=F(b)?b:!0;d=F(d)?d:!0;for(var e=a.length,f=0;f<e;f++){var g=a[f];this.updateEvent(g.id,g,!1,!1)}d&&z("onEventsUpdated",a);b&&(wb(),ra(),la())}};this.updateEvent=function(a,b,d,e){var f=!1;!t&&(f=this.removeEvent(a,!1,!1))&&(d=F(d)?d:!0,e=F(e)?e:!0,(f=this.addEvent(b,d,!1))&&e&&z("onEventUpdated",b));return f};
+this.updateEventDateTimes=function(a,b,d,e,f,g){var h=!1;t||(f=F(f)?f:!0,g=F(g)?g:!0,Sa(function(l){if(l.id===a)return l.from=b,l.to=d,l.repeatEnds=e,h=!0,g&&z("onEventUpdated",l),f&&(wb(),ra(),la()),!0}));return h};this.removeEvent=function(a,b,d){var e=!1;t||(b=F(b)?b:!0,d=F(d)?d:!0,Sa(function(f,g,h){if(h===a)return delete Ja[g][h],e=!0,d&&z("onEventRemoved",f),b&&(wb(),ra(),la()),!0}));return e};this.clearEvents=function(a,b){t||(a=F(a)?a:!0,b=F(b)?b:!0,Ja={},b&&sb("onEventsCleared"),a&&(wb(),
+ra(),la()))};this.getEvents=function(){var a=[];t||(a=Nb(Jc()));return a};this.getEvent=function(a){var b=null;ja(a)&&!t&&Sa(function(d){if(d.id===a)return b=d,!0});return b};this.removeExpiredEvents=function(a,b){t||(a=F(a)?a:!0,b=F(b)?b:!0,Sa(function(d){I(d.repeatEvery)===x.never&&d.to<new Date&&qa.removeEvent(d.id,!1,b)}),a&&(wb(),ra(),la()))};this.addEventType=function(a,b){var d=!1;xa(a)&&ja(b)&&!t&&!K.hasOwnProperty(a)&&(K[a]={text:b,eventEditorInput:null},r(L.visibleEventTypes)&&L.visibleEventTypes.push(a),
+d=!0);return d};this.removeEventType=function(a){var b=!1;xa(a)&&!t&&K.hasOwnProperty(a)&&(delete K[a],b=!0);return b};this.setVisibleEventTypes=function(a,b){if(za(a)&&!t){b=F(b)?b:!0;L.visibleEventTypes=[];for(var d=a.length,e=0;e<d;e++)-1===L.visibleEventTypes.indexOf(a[e])&&L.visibleEventTypes.push(a[e]);ta(!0,!1);b&&sb("onVisibleEventTypesChanged",L.visibleEventTypes)}};this.getAllGroups=function(){return zh()};this.clearAllGroups=function(a,b){t||(a=F(a)?a:!0,b=F(b)?b:!0,Sa(function(d){d.group=
+null}),b&&sb("onGroupsCleared"),a&&(wb(),ra(),la()))};this.removeGroup=function(a,b,d){if(ja(a)&&!t){b=F(b)?b:!0;d=F(d)?d:!0;var e=a.toLowerCase();Sa(function(f){null!==f.group&&f.group.toLowerCase()===e&&(f.group=null)});d&&sb("onGroupRemoved",a);b&&(wb(),ra(),la())}};this.setVisibleGroups=function(a,b){if(za(a)&&!t){b=F(b)?b:!0;L.visibleGroups=[];for(var d=a.length,e=0;e<d;e++){var f=a[e].toLowerCase();-1===L.visibleGroups.indexOf(f)&&L.visibleGroups.push(f)}ta(!0,!1);b&&sb("onVisibleGroupsChanged",
+L.visibleGroups)}};this.setClipboardEvent=function(a){ac(a)&&!t&&(X=[hd(a)])};this.setClipboardEvents=function(a){if(za(a)&&!t){X=[];for(var b=a.length,d=0;d<b;d++)X.push(hd(a[d]))}};this.getClipboardEvents=function(){var a=null;t||(a=X);return a};this.clearClipboard=function(){t||(X=[])};this.getVersion=function(){return"2.0.12"};this.getId=function(){return ia};this.setOptions=function(a,b){a=Uf(a);for(var d in a)a.hasOwnProperty(d)&&(c[d]=a[d]);dh();Sg();db&&((b=F(b)?b:!0)&&z("onOptionsUpdated",
+c),db=!1,t&&!Ba||Z(A,!0,!0))};this.setSearchOptions=function(a,b){if(!t){a=Uf(a);b=F(b)?b:!0;Id();for(var d in a)a.hasOwnProperty(d)&&(u[d]=a[d]);b&&z("onSearchOptionsUpdated",u)}};this.addHolidays=function(a,b,d){za(a)&&!t&&(b=F(b)?b:!0,d=F(d)?d:!0,c.holidays=c.holidays.concat(a),b&&z("onOptionsUpdated",c),d&&Z(A,!0,!0))};this.removeHolidays=function(a,b,d){if(za(a)&&!t){b=F(b)?b:!0;d=F(d)?d:!0;for(var e=c.holidays.length,f=[],g=0;g<e;g++){var h=c.holidays[g],l=C(h.title,n.empty);-1===a.indexOf(l)&&
+f.push(h)}c.holidays=f;b&&z("onOptionsUpdated",c);d&&Z(A,!0,!0)}};this.getHolidays=function(){return c.holidays};(function(a,b){B=a;Ga=b;ia=Hi;if(ja(ia)||bh(ia))c=Uf(Ii),c.showDayNumberOrdinals=H(c.showDayNumberOrdinals,!0),c.dragAndDropForEventsEnabled=H(c.dragAndDropForEventsEnabled,!0),c.maximumEventsPerDayDisplay=Ea(c.maximumEventsPerDayDisplay,3),c.exportEventsEnabled=H(c.exportEventsEnabled,!0),c.manualEditingEnabled=H(c.manualEditingEnabled,!0),c.showTimesInMainCalendarEvents=H(c.showTimesInMainCalendarEvents,
+!1),c.autoRefreshTimerDelay=Ea(c.autoRefreshTimerDelay,3E4),c.fullScreenModeEnabled=H(c.fullScreenModeEnabled,!0),c.eventTooltipDelay=Ea(c.eventTooltipDelay,1E3),c.minimumDayHeight=Ea(c.minimumDayHeight,0),c.holidays=Wg(c.holidays,[{day:1,month:1,title:"New Year's Day",onClickUrl:"https://en.wikipedia.org/wiki/New_Year%27s_Day"},{day:14,month:2,title:"Valentine's Day",onClickUrl:"https://en.wikipedia.org/wiki/Valentine%27s_Days"},{day:1,month:4,title:"April Fools' Day",onClickUrl:"https://en.wikipedia.org/wiki/April_Fools%27_Day"},
+{day:22,month:4,title:"Earth Day",onClickUrl:"https://en.wikipedia.org/wiki/Earth_Day"},{day:31,month:10,title:"Halloween",onClickUrl:"https://en.wikipedia.org/wiki/Halloween"},{day:11,month:11,title:"Remembrance Day",onClickUrl:"https://en.wikipedia.org/wiki/Remembrance_Day"},{day:24,month:12,title:"Christmas Eve",onClickUrl:"https://en.wikipedia.org/wiki/Christmas_Eve"},{day:25,month:12,title:"Christmas Day",onClickUrl:"https://en.wikipedia.org/wiki/Christmas"},{day:26,month:12,title:"Boxing Day",
+onClickUrl:"https://en.wikipedia.org/wiki/Boxing_Day"},{day:31,month:12,title:"New Year's Eve",onClickUrl:"https://en.wikipedia.org/wiki/New_Year%27s_Eve"}]),c.organizerName=m(c.organizerName,n.empty),c.organizerEmailAddress=m(c.organizerEmailAddress,n.empty),c.spacing=Ea(c.spacing,10),c.showAllDayEventDetailsInFullDayView=H(c.showAllDayEventDetailsInFullDayView,!1),c.showWeekNumbersInTitles=H(c.showWeekNumbersInTitles,!1),c.showTimelineArrowOnFullDayView=H(c.showTimelineArrowOnFullDayView,!0),c.maximumEventTitleLength=
+Ea(c.maximumEventTitleLength,0),c.maximumEventDescriptionLength=Ea(c.maximumEventDescriptionLength,0),c.maximumEventLocationLength=Ea(c.maximumEventLocationLength,0),c.maximumEventGroupLength=Ea(c.maximumEventGroupLength,0),c.eventNotificationsEnabled=H(c.eventNotificationsEnabled,!1),c.showPreviousNextMonthNamesInMainDisplay=H(c.showPreviousNextMonthNamesInMainDisplay,!0),c.showDayNamesInMainDisplay=H(c.showDayNamesInMainDisplay,!0),c.tooltipsEnabled=H(c.tooltipsEnabled,!0),c.useOnlyDotEventsForMainDisplay=
+H(c.useOnlyDotEventsForMainDisplay,!1),c.urlWindowTarget=m(c.urlWindowTarget,"_blank"),c.defaultEventBackgroundColor=m(c.defaultEventBackgroundColor,"#484848"),c.defaultEventTextColor=m(c.defaultEventTextColor,"#F5F5F5"),c.defaultEventBorderColor=m(c.defaultEventBorderColor,"#282828"),c.showExtraToolbarButtons=H(c.showExtraToolbarButtons,!0),c.showEmptyDaysInWeekView=H(c.showEmptyDaysInWeekView,!0),c.hideEventsWithoutGroupAssigned=H(c.hideEventsWithoutGroupAssigned,!1),c.showHolidays=H(c.showHolidays,
+!0),c.useTemplateWhenAddingNewEvent=H(c.useTemplateWhenAddingNewEvent,!0),c.useEscapeKeyToExitFullScreenMode=H(c.useEscapeKeyToExitFullScreenMode,!0),c.minimumDatePickerDate=Xg(c.minimumDatePickerDate,null),c.maximumDatePickerDate=Xg(c.maximumDatePickerDate,null),c.allowHtmlInDisplay=H(c.allowHtmlInDisplay,!1),c.datePickerSelectedDateFormat=m(c.datePickerSelectedDateFormat,"{d}{o} {mmmm} {yyyy}"),c.initialDateTime=Xg(c.initialDateTime,null),c.events=Wg(c.events,null),c.applyCssToEventsNotInCurrentMonth=
+H(c.applyCssToEventsNotInCurrentMonth,!0),c.weekendDays=bc(c.weekendDays,0)?[0,6]:c.weekendDays,c.addYearButtonsInDatePickerMode=H(c.addYearButtonsInDatePickerMode,!1),c.workingDays=bc(c.workingDays,0)?[]:c.workingDays,c.minimumYear=Ea(c.minimumYear,1900),c.maximumYear=Ea(c.maximumYear,2099),c.defaultEventDuration=Ea(c.defaultEventDuration,30),c.monthTitleBarDateFormat=m(c.monthTitleBarDateFormat,"{mmmm} {yyyy}"),c.configurationDialogEnabled=H(c.configurationDialogEnabled,!0),bc(c.visibleDays)&&(c.visibleDays=
+[0,1,2,3,4,5,6],hc=[]),F(c.allowEventScrollingOnMainDisplay)||(c.allowEventScrollingOnMainDisplay=!1,c.allowEventScrollingOnMainDisplay&&(c.maximumEventsPerDayDisplay=0)),bc(c.dayHeaderNames,7)&&(c.dayHeaderNames="Mon Tue Wed Thu Fri Sat Sun".split(" ")),bc(c.dayNames,7)&&(c.dayNames="Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" ")),bc(c.dayNamesAbbreviated,7)&&(c.dayNamesAbbreviated="Mon Tue Wed Thu Fri Sat Sun".split(" ")),bc(c.monthNames,12)&&(c.monthNames="January February March April May June July August September October November December".split(" ")),
+bc(c.monthNamesAbbreviated,12)&&(c.monthNamesAbbreviated="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ")),c.previousMonthTooltipText=m(c.previousMonthTooltipText,"Previous Month"),c.nextMonthTooltipText=m(c.nextMonthTooltipText,"Next Month"),c.previousDayTooltipText=m(c.previousDayTooltipText,"Previous Day"),c.nextDayTooltipText=m(c.nextDayTooltipText,"Next Day"),c.previousWeekTooltipText=m(c.previousWeekTooltipText,"Previous Week"),c.nextWeekTooltipText=m(c.nextWeekTooltipText,"Next Week"),
+c.addEventTooltipText=m(c.addEventTooltipText,"Add Event"),c.closeTooltipText=m(c.closeTooltipText,"Close"),c.exportEventsTooltipText=m(c.exportEventsTooltipText,"Export Events"),c.todayTooltipText=m(c.todayTooltipText,"Today"),c.refreshTooltipText=m(c.refreshTooltipText,"Refresh"),c.searchTooltipText=m(c.searchTooltipText,"Search"),c.expandDayTooltipText=m(c.expandDayTooltipText,"Expand Day"),c.listAllEventsTooltipText=m(c.listAllEventsTooltipText,"View All Events"),c.listWeekEventsTooltipText=m(c.listWeekEventsTooltipText,
+"View Current Week Events"),c.fromText=m(c.fromText,"From:"),c.toText=m(c.toText,"To:"),c.isAllDayText=m(c.isAllDayText,"Is All-Day"),c.titleText=m(c.titleText,"Title:"),c.descriptionText=m(c.descriptionText,"Description:"),c.locationText=m(c.locationText,"Location:"),c.addText=m(c.addText,"Add"),c.updateText=m(c.updateText,"Update"),c.cancelText=m(c.cancelText,"Cancel"),c.removeEventText=m(c.removeEventText,"Remove"),c.addEventTitle=m(c.addEventTitle,"Add Event"),c.editEventTitle=m(c.editEventTitle,
+"Edit Event"),c.exportStartFilename=m(c.exportStartFilename,"exported_events_"),c.fromTimeErrorMessage=m(c.fromTimeErrorMessage,"Please select a valid 'From' time."),c.toTimeErrorMessage=m(c.toTimeErrorMessage,"Please select a valid 'To' time."),c.toSmallerThanFromErrorMessage=m(c.toSmallerThanFromErrorMessage,"Please select a 'To' date that is larger than the 'From' date."),c.titleErrorMessage=m(c.titleErrorMessage,"Please enter a value in the 'Title' field (no empty space)."),c.stText=m(c.stText,
+"st"),c.ndText=m(c.ndText,"nd"),c.rdText=m(c.rdText,"rd"),c.thText=m(c.thText,"th"),c.yesText=m(c.yesText,"Yes"),c.noText=m(c.noText,"No"),c.allDayText=m(c.allDayText,"All-Day"),c.allEventsText=m(c.allEventsText,"All Events"),c.toTimeText=m(c.toTimeText,"to"),c.confirmEventRemoveTitle=m(c.confirmEventRemoveTitle,"Confirm Event Removal"),c.confirmEventRemoveMessage=m(c.confirmEventRemoveMessage,"Removing this event cannot be undone.  Do you want to continue?"),c.okText=m(c.okText,"OK"),c.exportEventsTitle=
+m(c.exportEventsTitle,"Export Events"),c.selectColorsText=m(c.selectColorsText,"Select Colors"),c.backgroundColorText=m(c.backgroundColorText,"Background Color:"),c.textColorText=m(c.textColorText,"Text Color:"),c.borderColorText=m(c.borderColorText,"Border Color:"),c.searchEventsTitle=m(c.searchEventsTitle,"Search Events"),c.previousText=m(c.previousText,"Previous"),c.nextText=m(c.nextText,"Next"),c.matchCaseText=m(c.matchCaseText,"Match Case"),c.repeatsText=m(c.repeatsText,"Repeats:"),c.repeatDaysToExcludeText=
+m(c.repeatDaysToExcludeText,"Repeat Days To Exclude:"),c.daysToExcludeText=m(c.daysToExcludeText,"Days To Exclude:"),c.seriesIgnoreDatesText=m(c.seriesIgnoreDatesText,"Series Ignore Dates:"),c.repeatsNever=m(c.repeatsNever,"Never"),c.repeatsEveryDayText=m(c.repeatsEveryDayText,"Every Day"),c.repeatsEveryWeekText=m(c.repeatsEveryWeekText,"Every Week"),c.repeatsEvery2WeeksText=m(c.repeatsEvery2WeeksText,"Every 2 Weeks"),c.repeatsEveryMonthText=m(c.repeatsEveryMonthText,"Every Month"),c.repeatsEveryYearText=
+m(c.repeatsEveryYearText,"Every Year"),c.repeatsCustomText=m(c.repeatsCustomText,"Custom:"),c.repeatOptionsTitle=m(c.repeatOptionsTitle,"Repeat Options"),c.moreText=m(c.moreText,"More"),c.includeText=m(c.includeText,"Include:"),c.minimizedTooltipText=m(c.minimizedTooltipText,"Minimize"),c.restoreTooltipText=m(c.restoreTooltipText,"Restore"),c.removeAllEventsInSeriesText=m(c.removeAllEventsInSeriesText,"Remove All Events In Series"),c.createdText=m(c.createdText,"Created:"),c.organizerNameText=m(c.organizerNameText,
+"Organizer:"),c.organizerEmailAddressText=m(c.organizerEmailAddressText,"Organizer Email:"),c.enableFullScreenTooltipText=m(c.enableFullScreenTooltipText,"Turn On Full-Screen Mode"),c.disableFullScreenTooltipText=m(c.disableFullScreenTooltipText,"Turn Off Full-Screen Mode"),c.idText=m(c.idText,"ID:"),c.expandMonthTooltipText=m(c.expandMonthTooltipText,"Expand Month"),c.repeatEndsText=m(c.repeatEndsText,"Repeat Ends:"),c.noEventsAvailableText=m(c.noEventsAvailableText,"No events available."),c.viewWeekEventsText=
+m(c.viewWeekEventsText,"View Week Events"),c.noEventsAvailableFullText=m(c.noEventsAvailableFullText,"There are no events available to view."),c.clickText=m(c.clickText,"Click"),c.hereText=m(c.hereText,"here"),c.toAddANewEventText=m(c.toAddANewEventText,"to add a new event."),c.weekText=m(c.weekText,"Week"),c.groupText=m(c.groupText,"Group:"),c.configurationTooltipText=m(c.configurationTooltipText,"Configuration"),c.configurationTitleText=m(c.configurationTitleText,"Configuration"),c.groupsText=m(c.groupsText,
+"Groups"),c.eventNotificationTitle=m(c.eventNotificationTitle,"Calendar.js"),c.eventNotificationBody=m(c.eventNotificationBody,"The event '{0' has started."),c.optionsText=m(c.optionsText,"Options:"),c.startsWithText=m(c.startsWithText,"Starts With"),c.endsWithText=m(c.endsWithText,"Ends With"),c.containsText=m(c.containsText,"Contains"),c.displayTabText=m(c.displayTabText,"Display"),c.enableAutoRefreshForEventsText=m(c.enableAutoRefreshForEventsText,"Enable auto-refresh for events"),c.enableBrowserNotificationsText=
+m(c.enableBrowserNotificationsText,"Enable browser notifications"),c.enableTooltipsText=m(c.enableTooltipsText,"Enable tooltips"),c.dayText=m(c.dayText,"day"),c.daysText=m(c.daysText,"days"),c.hourText=m(c.hourText,"hour"),c.hoursText=m(c.hoursText,"hours"),c.minuteText=m(c.minuteText,"minute"),c.minutesText=m(c.minutesText,"minutes"),c.enableDragAndDropForEventText=m(c.enableDragAndDropForEventText,"Enable drag & drop for events"),c.organizerTabText=m(c.organizerTabText,"Organizer"),c.removeEventsTooltipText=
+m(c.removeEventsTooltipText,"Remove Events"),c.confirmEventsRemoveTitle=m(c.confirmEventsRemoveTitle,"Confirm Events Removal"),c.confirmEventsRemoveMessage=m(c.confirmEventsRemoveMessage,"Removing these non-repeating events cannot be undone.  Do you want to continue?"),c.eventText=m(c.eventText,"Event"),c.optionalText=m(c.optionalText,"Optional"),c.urlText=m(c.urlText,"Url:"),c.openUrlText=m(c.openUrlText,"Open Url"),c.enableDayNameHeadersInMainDisplayText=m(c.enableDayNameHeadersInMainDisplayText,
+"Enable day name headers in the main display"),c.thisWeekTooltipText=m(c.thisWeekTooltipText,"This Week"),c.dailyText=m(c.dailyText,"Daily"),c.weeklyText=m(c.weeklyText,"Weekly"),c.monthlyText=m(c.monthlyText,"Monthly"),c.yearlyText=m(c.yearlyText,"Yearly"),c.repeatsByCustomSettingsText=m(c.repeatsByCustomSettingsText,"By Custom Settings"),c.lastUpdatedText=m(c.lastUpdatedText,"Last Updated:"),c.advancedText=m(c.advancedText,"Advanced"),c.copyText=m(c.copyText,"Copy"),c.pasteText=m(c.pasteText,"Paste"),
+c.duplicateText=m(c.duplicateText,"Duplicate"),c.showAlertsText=m(c.showAlertsText,"Show Alerts"),c.selectDatePlaceholderText=m(c.selectDatePlaceholderText,"Select date..."),c.hideDayText=m(c.hideDayText,"Hide Day"),c.notSearchText=m(c.notSearchText,"Not (opposite)"),c.showEmptyDaysInWeekViewText=m(c.showEmptyDaysInWeekViewText,"Show empty days in the week view"),c.showHolidaysInTheDisplaysText=m(c.showHolidaysInTheDisplaysText,"Show holidays in the main display and title bars"),c.newEventDefaultTitle=
+m(c.newEventDefaultTitle,"* New Event"),c.urlErrorMessage=m(c.urlErrorMessage,"Please enter a valid Url in the 'Url' field (or leave blank)."),c.searchTextBoxPlaceholder=m(c.searchTextBoxPlaceholder,"Search title, description, etc..."),c.currentMonthTooltipText=m(c.currentMonthTooltipText,"Current Month"),c.cutText=m(c.cutText,"Cut"),c.showMenuTooltipText=m(c.showMenuTooltipText,"Show Menu"),c.eventTypesText=m(c.eventTypesText,"Event Types"),c.lockedText=m(c.lockedText,"Locked:"),c.typeText=m(c.typeText,
+"Type:"),c.sideMenuHeaderText=m(c.sideMenuHeaderText,"Calendar.js"),c.sideMenuDaysText=m(c.sideMenuDaysText,"Days"),c.visibleDaysText=m(c.visibleDaysText,"Visible Days"),c.previousYearTooltipText=m(c.previousYearTooltipText,"Previous Year"),c.nextYearTooltipText=m(c.nextYearTooltipText,"Next Year"),c.showOnlyWorkingDaysText=m(c.showOnlyWorkingDaysText,"Show Only Working Days"),c.exportFilenamePlaceholderText=m(c.exportFilenamePlaceholderText,"Name (optional)"),c.errorText=m(c.errorText,"Error"),c.exportText=
+m(c.exportText,"Export"),xe(c.eventTypeNormalText,"Normal",0),xe(c.eventTypeMeetingText,"Meeting",1),xe(c.eventTypeBirthdayText,"Birthday",2),xe(c.eventTypeHolidayText,"Holiday",3),xe(c.eventTypeTaskText,"Task",4),Sg(),u=Uf(Ji,c.searchOptions),u.enabled=H(u.enabled,!0),u.lastSearchText=m(u.lastSearchText,n.empty),u.not=H(u.not,!1),u.matchCase=H(u.matchCase,!1),u.showAdvanced=H(u.showAdvanced,!1),u.searchTitle=H(u.searchTitle,!0),u.searchLocation=H(u.searchLocation,!1),u.searchDescription=H(u.searchDescription,
+!1),u.searchGroup=H(u.searchGroup,!1),u.searchUrl=H(u.searchUrl,!1),u.startsWith=H(u.startsWith,!1),u.endsWith=H(u.endsWith,!1),u.contains=H(u.contains,!0),u.left=Ea(u.left,null),u.top=Ea(u.top,null),u.history=Wg(u.history,[]),Z(c.initialDateTime,!0),null!==G&&F(c.openInFullScreenMode)&&c.openInFullScreenMode&&!t&&vh()})(document,window)};

文件差异内容过多而无法显示
+ 6 - 0
assets/js/easymde@2.18.0.min.js


文件差异内容过多而无法显示
+ 1 - 0
assets/js/sheetjs-0.20.0.full.min.js


+ 1 - 0
assets/langs/en.js

@@ -0,0 +1 @@
+export default{days:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysShort:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]};

+ 1 - 0
assets/langs/es.js

@@ -0,0 +1 @@
+export default{days:["Dom","Lun","Mar","Mie","Jue","Vie","Sab"],daysShort:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]};

+ 1 - 0
deploy.sh

@@ -0,0 +1 @@
+git clone https://x-token-auth:ATCTT3xFfGN025x1GSaC4NPcaYD5HMeOUGKHfgI91HfnsTkSM5lQ_sq3iGKZTxUmtgnp8qN2wXqBEprdXP6l-grpzYQpvxC4-Wd744A5W7MVmJBgQRuIIK-vE20qhb5kU2evkHdSlf05uYGMriWg7ChGMHNmHWUtViiocQ78XV8ZwaQJ9PX8gRs=2963B76F@bitbucket.org/swagonomics/kinbrio.git

+ 3 - 0
kinbot/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "git-blame.gitWebUrl": ""
+}

+ 7 - 0
kinbot/Cargo.lock

@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "kinbot"
+version = "0.1.0"

+ 8 - 0
kinbot/Cargo.toml

@@ -0,0 +1,8 @@
+[package]
+name = "kinbot"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]

+ 3 - 0
kinbot/matrix_bot/.env

@@ -0,0 +1,3 @@
+PW_MATRIX='2023!Deathstar123'
+UID_MATRIX='@santurce_software:matrix.org'
+MATRIX_COMMAND='kinbot'

+ 1 - 0
kinbot/matrix_bot/.gitignore

@@ -0,0 +1 @@
+/target

+ 3 - 0
kinbot/matrix_bot/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "git-blame.gitWebUrl": ""
+}

+ 3238 - 0
kinbot/matrix_bot/Cargo.lock

@@ -0,0 +1,3238 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[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 = "aead"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+dependencies = [
+ "cfg-if",
+ "cipher 0.4.4",
+ "cpufeatures",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom 0.2.10",
+ "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 = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "assign"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-once-cell"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72faff1fdc615a0199d7bf71e6f389af54d46a66e9beb5d76c39e48eda93ecce"
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backoff"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
+dependencies = [
+ "futures-core",
+ "getrandom 0.2.10",
+ "instant",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "tokio",
+]
+
+[[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 = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "blake3"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "digest 0.10.7",
+]
+
+[[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 = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[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 = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chacha20"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6"
+dependencies = [
+ "cfg-if",
+ "cipher 0.3.0",
+ "cpufeatures",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher 0.3.0",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "const-oid"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[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 = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "rand_core 0.5.1",
+ "serde",
+ "subtle",
+ "zeroize",
+]
+
+[[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 1.0.63",
+ "quote 1.0.29",
+ "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 1.0.29",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.12.3",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core 0.9.8",
+]
+
+[[package]]
+name = "der"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c"
+dependencies = [
+ "const-oid",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
+dependencies = [
+ "darling",
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
+dependencies = [
+ "derive_builder_core",
+ "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",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "serde",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand 0.7.3",
+ "serde",
+ "serde_bytes",
+ "sha2 0.9.9",
+ "zeroize",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[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 = "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",
+]
+
+[[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 = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "exr"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[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",
+ "nanorand",
+ "pin-project",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[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 = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[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-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "futures-signals"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a12cb78961d5c0bc0e358599bba98ec09201090a22339cd8ea27e815c11b25"
+dependencies = [
+ "discard",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "gensym",
+ "pin-project",
+]
+
+[[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-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[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 = "gensym"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb328fe25cbf075818a3e57bb5ee39b49b4f26c94d685356426154c5962cccd"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+ "uuid 0.7.4",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+
+[[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 = "half"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "hkdf"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
+dependencies = [
+ "hmac",
+]
+
+[[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 = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[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 = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder",
+ "num-rational",
+ "num-traits",
+ "png",
+ "qoi",
+ "tiff",
+]
+
+[[package]]
+name = "indexed_db_futures"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26ac735f676c52305becf53264b91cea9866a8de61ccbf464405b377b9cbca9"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "uuid 0.8.2",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg 1.1.0",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[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 = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
+
+[[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 = "jpeg-decoder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+dependencies = [
+ "rayon",
+]
+
+[[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 = "js_int"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "js_option"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg 1.1.0",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+
+[[package]]
+name = "lru"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909"
+dependencies = [
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "matrix-sdk"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbeafb4809f33f377165f2fbcf10e0613053ad206762194c3050a727fd3abcb2"
+dependencies = [
+ "anymap2",
+ "async-once-cell",
+ "async-stream",
+ "async-trait",
+ "backoff",
+ "bytes",
+ "dashmap",
+ "derive_builder",
+ "event-listener",
+ "futures-core",
+ "futures-signals",
+ "futures-util",
+ "http",
+ "matrix-sdk-base",
+ "matrix-sdk-common",
+ "matrix-sdk-indexeddb",
+ "matrix-sdk-sled",
+ "mime",
+ "reqwest",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "url",
+ "wasm-timer",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-base"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b944f6d1fc8779ba790dd0b942ceff45c626c1f5da847f01122d355ad06511bd"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "dashmap",
+ "futures-channel",
+ "futures-core",
+ "futures-signals",
+ "futures-util",
+ "lru",
+ "matrix-sdk-common",
+ "matrix-sdk-crypto",
+ "once_cell",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-common"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b85a6a743cc9dcf9385e61a26db78276078beddd27f3762d9d82baa2030695f1"
+dependencies = [
+ "async-lock",
+ "futures-core",
+ "futures-util",
+ "instant",
+ "ruma",
+ "serde",
+ "tokio",
+ "wasm-bindgen-futures",
+ "wasm-timer",
+]
+
+[[package]]
+name = "matrix-sdk-crypto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68fa699e8dd54578a4b92e3fcd18a50da8e415a0c042da1706b0330fc2d8f949"
+dependencies = [
+ "aes",
+ "async-trait",
+ "atomic",
+ "base64 0.13.1",
+ "byteorder",
+ "ctr",
+ "dashmap",
+ "event-listener",
+ "futures-util",
+ "hmac",
+ "matrix-sdk-common",
+ "pbkdf2",
+ "rand 0.8.5",
+ "ruma",
+ "serde",
+ "serde_json",
+ "sha2 0.10.7",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "vodozemac",
+ "zeroize",
+]
+
+[[package]]
+name = "matrix-sdk-indexeddb"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7847d36bba832bc787214323bc042b71dca7fdf2aee9f0e3eb573b64f2f7eb7f"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.13.1",
+ "dashmap",
+ "derive_builder",
+ "getrandom 0.2.10",
+ "indexed_db_futures",
+ "js-sys",
+ "matrix-sdk-base",
+ "matrix-sdk-crypto",
+ "matrix-sdk-store-encryption",
+ "ruma",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "matrix-sdk-sled"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ded5a703ad8a82b8edfde808228711315c8761a5fbf7ac2b98ab4951dadd066"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "dashmap",
+ "derive_builder",
+ "fs_extra",
+ "futures-core",
+ "futures-util",
+ "matrix-sdk-base",
+ "matrix-sdk-common",
+ "matrix-sdk-crypto",
+ "matrix-sdk-store-encryption",
+ "ruma",
+ "serde",
+ "serde_json",
+ "sled",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "matrix-sdk-store-encryption"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ddee75c3cca58f3a323283dc4e849d19d52988903f907ed0fb53dcad5d6fd25"
+dependencies = [
+ "blake3",
+ "chacha20poly1305",
+ "displaydoc",
+ "hmac",
+ "pbkdf2",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha2 0.10.7",
+ "thiserror",
+ "zeroize",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[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",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys",
+]
+
+[[package]]
+name = "nanorand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom 0.2.10",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[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 = "openssl"
+version = "0.10.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[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 0.9.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[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 0.3.5",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest 0.10.7",
+ "hmac",
+ "password-hash",
+ "sha2 0.10.7",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[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 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs7"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7364e6d0e236473de91e042395d71e0e64715f99a60620b014a4a4c7d1619b"
+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",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+dependencies = [
+ "proc-macro2 1.0.63",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc 0.1.0",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[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 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.10",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "random_poster"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "dotenv",
+ "image",
+ "matrix-sdk",
+ "mime",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "tokio",
+ "tracing-subscriber",
+ "url",
+]
+
+[[package]]
+name = "rayon"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
+
+[[package]]
+name = "reqwest"
+version = "0.11.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
+dependencies = [
+ "base64 0.21.2",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ruma"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dc348e3a4a18abc4e97fffa5e2e623f6edd50ba3a1dd5f47eb249fea713b69f"
+dependencies = [
+ "assign",
+ "js_int",
+ "js_option",
+ "ruma-client-api",
+ "ruma-common",
+ "ruma-federation-api",
+]
+
+[[package]]
+name = "ruma-client-api"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1e72bc731b4dc8b569aa83915f13e419144b67110d858c65bb74aa05e2dc4b7"
+dependencies = [
+ "assign",
+ "bytes",
+ "http",
+ "js_int",
+ "maplit",
+ "percent-encoding",
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ruma-common"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "716889595f4edc3cfeb94d9f122e413f73e37d7d80ea1c14196e1004241a3889"
+dependencies = [
+ "base64 0.13.1",
+ "bytes",
+ "form_urlencoded",
+ "getrandom 0.2.10",
+ "http",
+ "indexmap 1.9.3",
+ "itoa",
+ "js-sys",
+ "js_int",
+ "js_option",
+ "percent-encoding",
+ "rand 0.8.5",
+ "regex",
+ "ruma-identifiers-validation",
+ "ruma-macros",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tracing",
+ "url",
+ "uuid 1.4.0",
+ "wildmatch",
+]
+
+[[package]]
+name = "ruma-federation-api"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f905d12f6144c7a754bd0339fa6893698c03d03a908abb20cc6eeb4ec7f9466"
+dependencies = [
+ "js_int",
+ "ruma-common",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ruma-identifiers-validation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebefdab34311af44d07cd2cd91c36cfe6a8c770647c6b00f6ab47f1186b2bb72"
+dependencies = [
+ "js_int",
+ "thiserror",
+]
+
+[[package]]
+name = "ruma-macros"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f82e91eb61cd86d9287303133ee55b54618eccb75a522cc22a42c15f5bda340"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "ruma-identifiers-validation",
+ "serde",
+ "syn 1.0.109",
+ "toml",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustix"
+version = "0.37.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
+
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[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_bytes"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.166"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[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_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 = "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 = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[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 = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
+[[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 = "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.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27"
+dependencies = [
+ "der",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
+dependencies = [
+ "autocfg 1.1.0",
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[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.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
+dependencies = [
+ "autocfg 1.1.0",
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.12.1",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "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.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+
+[[package]]
+name = "toml_edit"
+version = "0.19.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
+dependencies = [
+ "indexmap 2.0.0",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "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 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[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-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "universal-hash"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[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 = "uuid"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+dependencies = [
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+dependencies = [
+ "getrandom 0.2.10",
+]
+
+[[package]]
+name = "uuid"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
+dependencies = [
+ "getrandom 0.2.10",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[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 = "vodozemac"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f20153a1c82ac5f1243b62e80f067ae608facc415c6ef82f88426a61c79886"
+dependencies = [
+ "aes",
+ "arrayvec",
+ "base64 0.13.1",
+ "cbc",
+ "ed25519-dalek",
+ "hkdf",
+ "hmac",
+ "pkcs7",
+ "prost",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "sha2 0.10.7",
+ "subtle",
+ "thiserror",
+ "x25519-dalek",
+ "zeroize",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[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",
+ "serde",
+ "serde_json",
+ "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 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+dependencies = [
+ "quote 1.0.29",
+ "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 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+ "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 = "wasm-timer"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
+dependencies = [
+ "futures",
+ "js-sys",
+ "parking_lot 0.11.2",
+ "pin-utils",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
+
+[[package]]
+name = "wildmatch"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86"
+
+[[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-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[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_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[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_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[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_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "winnow"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9482fe6ceabdf32f3966bfdd350ba69256a97c30253dc616fe0005af24f164e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "x25519-dalek"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.5.1",
+ "serde",
+ "zeroize",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2 1.0.63",
+ "quote 1.0.29",
+ "syn 2.0.23",
+]
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]

+ 19 - 0
kinbot/matrix_bot/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "random_poster"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+matrix-sdk = "*"
+url = "*"
+anyhow="*"
+tokio = { version = "*", features = ["full"] }
+mime="*"
+tracing-subscriber="*"
+dotenv="*"
+rand="*"
+image="*"
+serde_derive="*"
+serde="*"

+ 6 - 0
kinbot/matrix_bot/readme.md

@@ -0,0 +1,6 @@
+# matrix box example
+
+Using rust to create a bot that can listen and respond to messages. 
+
+PW_MATRIX -> access key for your user
+UID_MATRIX -> name of your user @blahblah:matrix.io

+ 87 - 0
kinbot/matrix_bot/src/main.rs

@@ -0,0 +1,87 @@
+use std::{env, fs};
+
+use image::EncodableLayout;
+use matrix_sdk::{
+    config::SyncSettings,
+    room::Room,
+    ruma::{
+        events::room::message::{MessageType, SyncRoomMessageEvent},
+        UserId,
+    },
+    Client, attachment::AttachmentConfig,
+};
+
+use serde_derive::Deserialize;
+use serde_derive::Serialize;
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Root {
+    pub model: Model,
+    pub messages: Vec<Message>,
+    pub key: String,
+    pub prompt: String,
+    pub temperature: i64,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Model {
+    pub id: String,
+    pub name: String,
+    pub max_length: i64,
+    pub token_limit: i64,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Message {
+    pub role: String,
+    pub content: String,
+}
+
+
+use rand::{rngs::StdRng, SeedableRng, Rng};
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    dotenv::dotenv()?;
+    let user_id_env = env::var("UID_MATRIX").expect("PW supplied");
+    let user_id = <&UserId>::try_from(user_id_env.as_str()).expect("parse user id");
+    let pw = env::var("PW_MATRIX").expect("PW supplied");
+    let client = Client::builder().user_id(user_id).build().await?;
+    client.login_username(user_id, &pw).send().await?;
+
+    let command_name = env::var("MATRIX_COMMAND").expect("MATRIX_COMMAND supplied");
+    let cmd = format!("!{}", command_name);
+
+    client.add_event_handler({
+        move |ev: SyncRoomMessageEvent, room: Room| {
+            let cmd = cmd.clone();
+            async move {
+                if let Room::Joined(room) = room {
+                    let MessageType::Text(ref text_content) = ev.as_original().expect("Get evt").content.msgtype else {
+                        return;
+                    };
+                    if text_content.body.contains(&cmd) {
+                        let mut rnd: StdRng = StdRng::from_entropy();
+
+                        // let range = rnd.gen_range(0..=images.len());
+                        // println!("{}", range);
+                        // let image = images.get(range).unwrap();
+                        // let path = format!("{}/{}", folder, image);
+                        // let img = fs::read(path).unwrap();
+                        // let fmt = match image {
+                        //     i if i.contains("jpg") => &mime::IMAGE_JPEG,
+                        //     i if i.contains("png") => &mime::IMAGE_PNG,
+                        //     i if i.contains("gif") => &mime::IMAGE_GIF,
+                        //     _ => &mime::IMAGE_JPEG,
+                        // };
+                        // room.send_attachment("",  fmt, img.as_bytes(), AttachmentConfig::new()).await.unwrap();
+                    }
+                }
+            }
+        }
+    });
+    client.sync(SyncSettings::default()).await?;
+    Ok(())
+}

+ 3 - 0
kinbot/src/main.rs

@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello, world!");
+}

二进制
logo_v1/banner.png


二进制
logo_v1/facebook_cover_photo_1.png


二进制
logo_v1/facebook_cover_photo_2.png


二进制
logo_v1/facebook_profile_image.png


二进制
logo_v1/favicon.png


二进制
logo_v1/instagram_profile_image.png


二进制
logo_v1/linkedin_banner_image_1.png


二进制
logo_v1/linkedin_banner_image_2.png


二进制
logo_v1/linkedin_profile_image.png


二进制
logo_v1/logo.png


二进制
logo_v1/logo_transparent.png


二进制
logo_v1/pinterest_board_photo.png


二进制
logo_v1/pinterest_profile_image.png


二进制
logo_v1/twitter_header_photo_1.png


二进制
logo_v1/twitter_header_photo_2.png


二进制
logo_v1/twitter_profile_image.png


二进制
logo_v1/youtube_profile_image.png


+ 49 - 0
marketing.md

@@ -0,0 +1,49 @@
+# ![kinbrio logo](logo_v1/banner.png)
+
+## Complete Business Management Solution
+
+### Unify Projects and Organization management with communications, accounting, client and customer management, inventory and more to streamline business.
+
+### Your communications and data are exportable at a moments notice and using the [matrix protocol](https://matrix.org/) to never lock your valuable internal chats into a vendor. 
+
+### Encrypted and able to be self hosted so privacy and (if your industry requires it!) the ability to be your own data custodian are at the forefront of our design. 
+
+## Communication
+
+Communication is the most important aspect of your business from teams and clients down to interpersonal note taking. We take that seriously and believe communication has to be organized and efficient while also being a living breathing historical part of your organization. 
+
+For real time communication, voice, video and screen sharing we chose [matrix](https://matrix.org/) a protocol that avoids tying you and your business to a single provider and [offers different web and desktop clients](https://matrix.org/ecosystem/clients/) for you to find the one _you_ like best.
+
+For email we use https://mailu.io/ to ensure your organization alerts, reports, updates and all related kinbrio information is transmitted to you, your team, suppliers and your customers without trackers and 3rd partys. This aids in our ability to enable you and your company the optional ability for on-site management of your email communication to run alongside kinbrio in a hardened environment. 
+
+## Organization Management
+
+Create and manage your organization, teams, communication channels and internal documentation. Branding and digital web presence can be easily deployed from the Kinbrio dashbaord.
+
+## Project Management
+
+Creating projects, tasks and milestones for your business shouldn't be a full time job. Scheduling and status tracking is stream lined and directly integrated into your internal communications so everyone is always on the same page. Whether you need a simple distributed to-do list for your team or a more sophisticated scheduling system we have your project covered.
+
+## Accounting Management
+
+Keeping track of simple invoices and sales is possible as well as integration with [Akaunting](https://akaunting.com/) to be able to use their platform for more involved accounting needs, or host your own [Instance](https://github.com/akaunting/akaunting) using their open and freely available software!
+
+## Client and Customer Management
+
+Manage your clients entitys and contact infromation with documentation and notes with historical context instantly. Collect point of sale customer information and store it securely and able to be used in your future marketing campaigns as well as easily comply with PII laws and requests to remove sensitive personal identifying information by your customers. For ongoing clients that require secure and encrypted communication you can create dedicated matrix rooms for your company and a client to securely and privately communicate
+
+## Supplier Management
+
+Tracking, meeting, scheduling and even interfacing with suppliers can be difficult to orchestrate so allowing your business to seamlessly schedule and report on suppliers across products, projects and more can help you optimize your cash flow as well as your brainload to make sure your products are delivered. 
+ 
+## Inventory management 
+
+Manage cost, pricing, quantities, notes and documentation of products you sell and supply. Keep track of customers and trends with easy sales reporting. Integrate with popular POS applications and online storefronts to help bring you from data to up and running in a moment.
+
+## In the age of smart AI powered bots 
+
+As your organization grows and your communication history grows into years help avoid long and painful searches of historical chats with powerful AI to answer your organizational questions quickly as well as help you to the best of it's ability with answering small questions without you having to break focus and start from scratch opening up a search engine and start researching.
+
+## Creating Your Presence
+
+Iterate on a new venture quickly by generating landing pages and product pages, store fronts, designs and more quickly - you style and brand your ideas and we bring them to life. We make sure you aren't left with a proof of concept, we offer services for SEO and content, fully features web applications and custom builds, digital advertising management and technical consulting to make sure your business is operating at full capacity as quickly as possible.

二进制
marketing.pdf


+ 116 - 0
readme.md

@@ -0,0 +1,116 @@
+# Kinbrio
+
+Business management platform to manage small business. Integration with LLama2 and Akaunting.
+
+`sudo apt update`
+
+`sudo apt upgrade`
+
+`sudo apt install build-essential`
+
+`curl https://sh.rustup.rs -sSf | sh`
+
+installs rust/cargo and voila you're done (https://doc.rust-lang.org/cargo/getting-started/installation.html)
+
+`sudo apt-get install postgresql-14`
+
+install postgresql
+
+    `sudo su - postgres`
+
+    `createuser projectmanager --pwprompt`
+     set password: `projectmanager123`
+
+    `createdb projectmanager`
+	
+	use the postgres cmd line client
+	`psql`
+	
+	`GRANT ALL PRIVILEGES ON DATABASE projectmanager TO projectmanager;`
+You can change the user and password in the createuser call and even the database in the createdb call, but the .env file has to reflect them in the DATABASE_URL variable so it points to what you have setup locally.
+
+
+# RUN IT
+
+to run the app it's `cargo run` same for the kinbot/matrix-bot project.
+
+`sh start-gpt.sh` will start a server for the AI assistant dashboard widget we frame in.
+`sh startt-matrix-bot.sh` 
+# SPECS 
+
+## Project management MVP
+Create an organization + user
+
+Add Users to your organization + assign them roles
+
+Add a project to your organization 
+
+Add tasks to a project
+
+Add a milestone to this project, time spent / particular or % of tasks completed / dates hit / etc 
+
+View list of tasks
+
+View calendar view of tasks for project
+
+View calendar view of tasks for organization
+
+Add a board to add visualizations for a project or for your organziation 
+    Used to select and display data in a custom way for the user 
+
+Everything can have a file or note attached to it and a way for the users to view them
+
+## Relationship manager MVP
+
+Create an external organization and contact(s)
+
+### Core Data:
+
+#### ENTITY (Business, Vendor, Client, Customer) 
+	-> Contact(s) Information
+	-> sensitive PII, jobs and processing for accessibility/exporting/integrating/scrambling
+
+#### Service Item (Item, service rendered, contactual obligation)
+	-> Name
+	-> Details
+	-> 
+
+#### Transaction (Point of sale, service completed, contract closed) 
+    these should be templatable, people will reuse more than create from scratch 
+    so the label/details/etc(this will surely grow), can be preconfigured 
+    (but overridable at creation))
+	-> Entity involved + point person(s) contact
+	-> Label
+	-> Details
+	-> final dollar amount if applicable
+	-> date expected vs date actual (service completion / contract closing will vary)
+	
+#### APPOINTMENT (Future Transaction)
+	-> Transaction
+	-> Date + Duration
+	-> Tentative dollar amount if applicable
+
+### Core features
+
+Matrix server integration - client integrations for workflow !automations
+
+Contact management (contacts/leads)
+
+Service/Item organization
+
+appointment management/project calenders
+
+Reporting/projections/deriving from data, potential vs actual, timelines etc
+
+### Relation management use case 
+Fencing company: Suppliers + Customers, appointments for upcoming jobs and deliveries
+
+T-Shirt company: Suppliers + Customers -> emailing promotions/ads, importing potential customers from lead generation campaigns
+
+Marketing broker: Lead generators and lead consumers, acquiring and forwarding lists of leads on to 3rd partys via API/Export/Etc
+
+Saas Service Provider: Customer and account integration via API to manage user accounts and schedule appointments for onboardings/support calls/etc
+
+Contractors: Customers, sub contractors, clients, potential clients + appointments for scheduling work and client deadlines.
+
+Software consulting business: Clients, potential clients, internal/external employees + appointment management, contracts potential/in flight/closed  

+ 1200 - 0
src/akaunting.rs

@@ -0,0 +1,1200 @@
+
+use std::ops::Mul;
+use std::str::FromStr;
+
+use base64::Engine;
+use base64::engine::GeneralPurposeConfig;
+use serde_derive::Deserialize;
+use serde_derive::Serialize;
+use serde_json::Value;
+use reqwest;
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use serde_this_or_that::as_bool;
+use tide::{http::mime, Request};
+use askama::Template;
+use tokio::join;
+use uuid::Uuid;
+use crate::common::BufferedBytesStream;
+use crate::entity::Entity;
+use crate::organization::{update_organization,get_organization};
+use crate::service_item::ServiceItem;
+use crate::{State, user};
+ 
+
+async fn update_akaunting_options(conn: &mut PoolConnection<Postgres>, akaunting_options: &AkauntingSyncOption) {
+    sqlx::query!("UPDATE akaunting_options 
+    SET organization_data=$1, employee_data=$2, client_data=$3, 
+    vendor_data=$4, item_data=$5,invoice_data=$6, allow_post=$7, last_sync=$8, user_name=$9, user_pass=$10, akaunting_domain=$11, akaunting_company_id=$12 where key=$13",  
+    &akaunting_options.organization_data,
+    &akaunting_options.employee_data,
+    &akaunting_options.client_data, 
+    &akaunting_options.vendor_data, 
+    &akaunting_options.item_data, 
+    &akaunting_options.invoice_data, 
+    &akaunting_options.allow_post, 
+    &akaunting_options.last_sync, 
+    &akaunting_options.user_name, 
+    &akaunting_options.user_pass, 
+    &akaunting_options.akaunting_domain, 
+    &akaunting_options.akaunting_company_id, 
+    akaunting_options.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+pub async fn get_akaunting_options(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Option<AkauntingSyncOption> {
+    match sqlx::query!(
+        "select key, organization_key, owner_key,  user_name, user_pass, akaunting_domain, akaunting_company_id, organization_data, employee_data, client_data, vendor_data, item_data, invoice_data, allow_post, last_sync, created, updated from akaunting_options where organization_key = $1",
+        organization_key
+    )
+    .fetch_one(conn)
+    .await {
+        Ok(akaunting_options) => Some(AkauntingSyncOption {
+            key: akaunting_options.key.expect("key exists"),
+            organization_key: akaunting_options.organization_key.expect("organization_key"), 
+            owner_key: akaunting_options.owner_key.expect("owner_key exists"),
+            user_name: akaunting_options.user_name.unwrap_or_default(),
+            user_pass: akaunting_options.user_pass.unwrap_or_default(),
+            akaunting_domain: akaunting_options.akaunting_domain.unwrap_or_default(),
+            akaunting_company_id: akaunting_options.akaunting_company_id.unwrap_or_default(),
+            organization_data: akaunting_options.organization_data.expect("organization_data exists"),
+            employee_data: akaunting_options.employee_data.expect("employee_data exists"),
+            client_data: akaunting_options.client_data.expect("client_data exists"), 
+            vendor_data: akaunting_options.vendor_data.expect("vendor_data exists"), 
+            item_data: akaunting_options.item_data.expect("item_data"),
+            invoice_data: akaunting_options.invoice_data.expect("invoice_data"),
+            allow_post: akaunting_options.allow_post.expect("allow_post"),
+            last_sync: akaunting_options.last_sync.expect("last_sync"),
+            created: akaunting_options.created.expect("created exists"),
+            updated: akaunting_options.updated.expect("updated exists"),
+        }),
+        Err(_) => {
+            None
+        }
+    }
+
+    
+}
+async fn insert_akaunting_options(conn: &mut PoolConnection<Postgres>, new_akaunting_options: &AkauntingSyncOption) {
+    sqlx::query!("INSERT INTO akaunting_options (key, organization_key, owner_key,  user_name, user_pass,  akaunting_domain, akaunting_company_id, organization_data, employee_data, client_data, vendor_data, item_data, invoice_data, allow_post, last_sync, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)", 
+        new_akaunting_options.key,
+        new_akaunting_options.organization_key, 
+        new_akaunting_options.owner_key,
+        new_akaunting_options.user_name,
+        new_akaunting_options.user_pass,
+        new_akaunting_options.akaunting_domain,
+        new_akaunting_options.akaunting_company_id,
+        &new_akaunting_options.organization_data,
+        &new_akaunting_options.employee_data,
+        &new_akaunting_options.client_data, 
+        &new_akaunting_options.vendor_data, 
+        &new_akaunting_options.item_data, 
+        &new_akaunting_options.invoice_data, 
+        &new_akaunting_options.allow_post, 
+        &new_akaunting_options.last_sync, 
+        new_akaunting_options.created,
+        new_akaunting_options.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+pub async fn save_akaunting_options(mut req: Request<State>) -> tide::Result {
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    let umd: Result<AkauntingSyncOption, tide::Error> = req.body_json().await;
+    match umd {
+        Ok(akaunting_options) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            if akaunting_options.key == uuid::Uuid::nil() {
+                let ao = akaunting_options.clone();
+                let s = AkauntingSyncOption::new(akaunting_options.organization_key, akaunting_options.owner_key, akaunting_options.user_name, 
+                    akaunting_options.user_pass, akaunting_options.akaunting_domain, akaunting_options.akaunting_company_id, akaunting_options.organization_data,
+                     akaunting_options.employee_data, akaunting_options.client_data, akaunting_options.vendor_data, akaunting_options.item_data, akaunting_options.invoice_data, akaunting_options.allow_post,
+                    akaunting_options.last_sync);
+                insert_akaunting_options(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                let mut org = get_organization(&mut conn, organization_key).await; 
+                let akaunting_companys = ao.list_companys().await.data;
+                for c in akaunting_companys {
+                    if c.id.to_string() == ao.akaunting_company_id {
+                        org.name = c.name;
+                        break 
+                    }
+                }
+                update_organization(&mut conn, &org).await;
+                let j = serde_json::to_string(&s).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            } else {
+                update_akaunting_options(&mut conn, &akaunting_options).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                let mut org = get_organization(&mut conn, organization_key).await; 
+                let ao = akaunting_options.clone();
+                let akaunting_companys = ao.list_companys().await.data;
+                for c in akaunting_companys {
+                    if c.id.to_string() == ao.akaunting_company_id {
+                        org.name = c.name;
+                        org.contact_email = c.email;
+                        org.external_accounting_url = ao.akaunting_domain;
+                        org.external_accounting_id = ao.akaunting_company_id;
+                        break 
+                    }
+                }
+                update_organization(&mut conn, &org).await;
+                let j = serde_json::to_string(&akaunting_options).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct AkauntingImportByID {
+    import_id: String,
+}
+
+pub async fn import_item(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    let mime = req.content_type().unwrap();
+    let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+
+    if mime.essence().to_string() == "multipart/form-data" {
+        let boundary = mime.param("boundary").unwrap().to_string();
+        let mut body = BufferedBytesStream { inner: req };
+        let mut multipart = multer::Multipart::new(&mut body, boundary);
+        let mut import_id = "".to_string();
+        while let Some(field) = multipart.next_field().await.expect("next field") {
+            let f_name = field.name().clone().expect("get field name");
+            if f_name == "import_id" {
+                import_id = field.text().await.expect("import_id multi field");
+            }
+        } 
+        let mut options = match get_akaunting_options(&mut conn, u.organization_key).await {
+            Some(options) => options,
+            None => {
+                let s = AkauntingSyncOption::new(u.organization_key, u.key, "".to_string(),"".to_string(),"".to_string(),"".to_string(),true, true, true, true, true, true, true, 0);
+                insert_akaunting_options(&mut conn, &s).await;
+                s
+            }
+        };
+        let items = options.list_items();
+        if import_id.len() == 0 {
+            return Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("bad import_id")
+                .build());
+        }
+        let item = options.get_item(import_id); 
+        let (items, idata) = join!(items, item);
+        let idata = idata.data;
+        let svc_type = match idata.type_field.as_str() {
+            "Service" => crate::service_item::ServiceItemType::Service,
+            "Item"=> crate::service_item::ServiceItemType::Item,
+            _ => crate::service_item::ServiceItemType::Item,
+        };
+        let svc_item = ServiceItem::new(u.organization_key, 
+            idata.id.to_string(), 
+            u.key, 
+            idata.name, 
+            idata.description,
+            idata.sale_price as i64, 
+            "USD".to_string(), 
+            svc_type,
+            crate::service_item::ServiceValueType::Full, 
+            vec![]);
+            crate::service_item::insert_service_item(&mut conn, &svc_item).await;
+        if options.akaunting_domain.len() == 0 {
+            options.akaunting_domain = "https://app.akaunting.com/api".to_string()
+        }
+        let org = crate::organization::get_organization(&mut conn, u.organization_key);
+        let companys = options.list_companys();
+        let invoices = options.list_invoices();
+        let customers = options.list_customers();
+        let users = options.list_users();
+        let (org, companys, invoices, customers, users) = join!(org, companys, invoices, customers, users);
+        Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    AkauntingSyncOptionTemplate::new(
+                        options,
+                        u,
+                        org,
+                        companys.data,
+                        items.data,
+                        invoices.data,
+                        customers.data,
+                        users.data,
+                    )
+                    .render_string(),
+                )
+                .build())
+        } else  {
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid form body'}")
+                .build())
+        }
+}
+
+pub async fn import_customer(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    let mime = req.content_type().unwrap();
+    let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+
+    if mime.essence().to_string() == "multipart/form-data" {
+        let boundary = mime.param("boundary").unwrap().to_string();
+        let mut body = BufferedBytesStream { inner: req };
+        let mut multipart = multer::Multipart::new(&mut body, boundary);
+        let mut import_id = "".to_string();
+        while let Some(field) = multipart.next_field().await.expect("next field") {
+            let f_name = field.name().clone().expect("get field name");
+            if f_name == "import_id" {
+                import_id = field.text().await.expect("import_id multi field");
+            }
+        } 
+        let mut options = match get_akaunting_options(&mut conn, u.organization_key).await {
+            Some(options) => options,
+            None => {
+                let s = AkauntingSyncOption::new(u.organization_key, u.key, "".to_string(),"".to_string(),"".to_string(),"".to_string(),true, true, true, true, true, true, true, 0);
+                insert_akaunting_options(&mut conn, &s).await;
+                s
+            }
+        };
+        if import_id.len() == 0 {
+            return Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("bad import_id")
+                .build());
+        }
+        let customers = options.list_customers();
+        let customer = options.get_customer(import_id); 
+        let (customers, customer) = join!(customers, customer);
+        let idata = customer.data; 
+        let ent = Entity::new(u.organization_key, 
+            idata.id.to_string(),
+            u.key, 
+            idata.name, 
+            String::default(), 
+            String::default(), 
+            idata.website.to_string(), 
+            String::default(),
+            crate::entity::EntityType::Client, 
+            idata.address.to_string(),
+            String::default(),
+            String::default(),
+            String::default(),
+            String::default(),
+            String::default(),
+            );
+            crate::entity::insert_entity(&mut conn, &ent).await;
+        if options.akaunting_domain.len() == 0 {
+            options.akaunting_domain = "https://app.akaunting.com/api".to_string()
+        }
+        let org = crate::organization::get_organization(&mut conn, u.organization_key);
+        let companys = options.list_companys();
+        let invoices = options.list_invoices();
+        let items = options.list_items();
+        let users = options.list_users();
+        let (org, companys, invoices, items, users) = join!(org, companys, invoices, items, users);
+        Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    AkauntingSyncOptionTemplate::new(
+                        options,
+                        u,
+                        org,
+                        companys.data,
+                        items.data,
+                        invoices.data,
+                        customers.data,
+                        users.data,
+                    )
+                    .render_string(),
+                )
+                .build())
+        } else  {
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid form body'}")
+                .build())
+        }
+}
+
+pub async fn get_akaunting_options_page(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+    let org: crate::organization::Organization = crate::organization::get_organization(&mut conn, u.organization_key).await;
+    let mut options = match get_akaunting_options(&mut conn, u.organization_key).await {
+        Some(options) => options,
+        None => {
+            let s = AkauntingSyncOption::new(u.organization_key, u.key, "".to_string(),"".to_string(),"".to_string(),"".to_string(),true, true, true, true, true, true, true, 0);
+            insert_akaunting_options(&mut conn, &s).await;
+            s
+        }
+    };
+    if options.akaunting_domain.len() == 0 {
+        options.akaunting_domain = "https://app.akaunting.com/api".to_string()
+    }
+    let companys = options.list_companys().await;
+    let items = options.list_items().await;
+    let invoices = options.list_invoices().await;
+    let customers = options.list_customers().await;
+    let users = options.list_users().await;
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(
+            AkauntingSyncOptionTemplate::new(
+                options,
+                u,
+                org,
+                companys.data,
+                items.data,
+                invoices.data,
+                customers.data,
+                users.data,
+            )
+            .render_string(),
+        )
+        .build())
+} 
+
+#[derive(Debug, Deserialize, Serialize, Default)]
+pub struct AkauntingError {
+    pub message: String,
+    pub status_code: String,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct AkauntingSyncOption {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    
+    pub user_name: String,
+    pub user_pass: String,
+    pub akaunting_domain: String,
+    pub akaunting_company_id: String,
+
+    #[serde(deserialize_with = "as_bool")]
+    pub organization_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub employee_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub client_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub vendor_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub item_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub invoice_data: bool,
+    #[serde(deserialize_with = "as_bool")]
+    pub allow_post: bool,
+
+    pub last_sync: i64,
+    pub created: i64,
+    pub updated: i64,
+}
+
+impl AkauntingSyncOption {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        user_name: String,
+        user_pass: String,
+        akaunting_domain: String,
+        akaunting_company_id: String,
+        organization_data: bool,
+        employee_data: bool,
+        client_data: bool,
+        vendor_data: bool,
+        item_data: bool,
+        invoice_data: bool,
+        allow_post: bool,
+        last_sync: i64,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            owner_key,
+            user_name,
+            user_pass,
+            akaunting_domain,
+            akaunting_company_id,
+            organization_data,
+            employee_data,
+            client_data,
+            vendor_data,
+            item_data,
+            invoice_data,
+            allow_post,
+            last_sync,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "akaunting.html")]
+pub struct AkauntingSyncOptionTemplate {
+    akaunting_options: AkauntingSyncOption,
+    user: crate::user::User,
+    organization: crate::organization::Organization,
+    companys: Vec<CompanyData>,
+    items: Vec<ItemData>,
+    invoices: Vec<InvoiceData>,
+    customers: Vec<CustomerData>,
+    users: Vec<UserData>,
+}
+impl<'a> AkauntingSyncOptionTemplate {
+    pub fn new(
+        akaunting_options: AkauntingSyncOption,
+        user: crate::user::User,
+        organization: crate::organization::Organization,
+        companys: Vec<CompanyData>,
+        items: Vec<ItemData>,
+        invoices: Vec<InvoiceData>,
+        customers: Vec<CustomerData>,
+        users: Vec<UserData>,
+    ) -> Self { 
+        return Self {
+            akaunting_options,
+            user,
+            organization,
+            companys,
+            items,
+            invoices,
+            customers,
+            users,
+        };
+    }
+
+    pub fn get_str(&self, attr: &Option<String>) -> String {
+        let s = attr.clone().unwrap_or_default();
+        s.clone()
+    } 
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    } 
+}
+ 
+fn akaunting_unmarshal<'a, T: serde::Deserialize<'a>>(body: &'a str) -> Result<T, AkauntingError> {
+    match serde_json::from_str(body) {
+        Ok(v) => Ok(v),
+        Err(e) => {
+            let error_response: AkauntingError = serde_json::from_str(body).unwrap_or_default();
+            println!("{:?}", e);
+            println!("{:?}", error_response);
+            Err(error_response)
+        }
+    }
+}
+
+impl AkauntingSyncOption {
+    fn get_authorization(&self) -> String { 
+        let mut buf = String::new();
+        let eng = base64::engine::GeneralPurpose::new(&base64::alphabet::URL_SAFE, GeneralPurposeConfig::new());
+        eng.encode_string(format!("{}:{}", self.user_name, self.user_pass), &mut buf);
+        let s = format!("Basic {}", buf);
+        s
+    }
+    
+    pub fn can_sync(&self) -> bool {
+        self.akaunting_domain.len() == 0 || self.user_name.len() == 0 || self.user_pass.len() == 0
+    }
+
+    pub async fn list_companys(&self) -> ListCompanys {
+        if self.can_sync() {
+            return ListCompanys{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/companies",self.akaunting_domain)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+
+    pub async fn get_customer(&self, id: String) -> GetContactData {
+        if self.can_sync() {
+            return GetContactData{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/contacts/{}?search=type%3Acustomer",self.akaunting_domain, id)
+        )
+        .header("Authorization", self.get_authorization())
+        .header("X-Company", self.akaunting_company_id.to_string());
+    
+        let response = request.send().await.expect("Sent request");
+        let body: String = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+    pub async fn get_item(&self, id: String) -> Item {
+        if self.can_sync() {
+            return Item{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/items/{}",self.akaunting_domain, id)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body: String = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+
+    pub async fn list_items(&self) -> ListItems {
+        if self.can_sync() {
+            return ListItems{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/items",self.akaunting_domain)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body: String = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+    pub async fn list_invoices(&self) -> ListInvoices {
+        if self.can_sync() {
+            return ListInvoices{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/documents?search=type:invoice&page=1&limit=50",self.akaunting_domain)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+    pub async fn list_customers(&self) -> ListCustomers {
+        if self.can_sync() {
+            return ListCustomers{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/contacts?search=type:customer&page=1&limit=25",self.akaunting_domain)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+    pub async fn list_users(&self) -> ListUserData {
+        if self.can_sync() {
+            return ListUserData{..Default::default()}
+        }
+        let client = reqwest::Client::builder().build().expect("Built client");
+    
+        let request = client.request(
+            reqwest::Method::GET,
+            format!("{}/users",self.akaunting_domain)
+        ).header("Authorization", self.get_authorization());
+        let response = request.send().await.expect("Sent request");
+        let body = response.text().await.expect("Receive body");
+        return akaunting_unmarshal(body.as_str()).unwrap_or_default();
+    }
+} 
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Item {
+    pub data: GetItemData
+}
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListUserData {
+    pub data: Vec<UserData>,
+    pub links: Links,
+    pub meta: Meta,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UserData {
+    pub id: i64,
+    pub name: String,
+    pub email: String,
+    pub locale: String,
+    #[serde(rename = "landing_page")]
+    pub landing_page: String,
+    pub enabled: bool,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "last_logged_in_at")]
+    pub last_logged_in_at: String,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+    pub companies: Companies,
+    pub roles: Roles,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Companies {
+    pub data: Vec<Company>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Company {
+    pub id: i64,
+    pub name: String,
+    pub email: String,
+    pub currency: String,
+    pub domain: String,
+    pub address: String,
+    pub logo: String,
+    pub enabled: bool,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: i64,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Roles {
+    pub data: Vec<Role>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Role {
+    pub id: i64,
+    pub name: String,
+    pub code: String,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetContactData {
+    pub data: Contact,
+} 
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListCustomers {
+    pub data: Vec<CustomerData>,
+    pub links: Links,
+    pub meta: Meta,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomerData {
+    pub id: i64,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "user_id")]
+    pub user_id: Value,
+    #[serde(rename = "type")]
+    pub type_field: Option<String>,
+    pub name: String,
+    pub email: Option<String>,
+    #[serde(rename = "tax_number")]
+    pub tax_number: Value,
+    pub phone: Value,
+    pub address: Option<String>,
+    pub website: Value,
+    #[serde(rename = "currency_code")]
+    pub currency_code: Option<String>,
+    pub enabled: bool,
+    pub reference: Value,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: Option<String>,
+    #[serde(rename = "updated_at")]
+    pub updated_at: Option<String>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListInvoices {
+    pub data: Vec<InvoiceData>,
+    pub links: Links,
+    pub meta: Meta,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InvoiceData {
+    pub id: i64,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "type")]
+    pub type_field:  Option<String>,
+    #[serde(rename = "document_number")]
+    pub document_number:  Option<String>,
+    #[serde(rename = "order_number")]
+    pub order_number:  Option<String>,
+    pub status:  Option<String>,
+    #[serde(rename = "issued_at")]
+    pub issued_at:  Option<String>,
+    #[serde(rename = "due_at")]
+    pub due_at:  Option<String>,
+    pub amount: f64,
+    #[serde(rename = "amount_formatted")]
+    pub amount_formatted:  Option<String>,
+    #[serde(rename = "category_id")]
+    pub category_id: Option<i64>,
+    #[serde(rename = "currency_code")]
+    pub currency_code:  Option<String>,
+    #[serde(rename = "currency_rate")]
+    pub currency_rate: Option<i64>,
+    #[serde(rename = "contact_id")]
+    pub contact_id: Option<i64>,
+    #[serde(rename = "contact_name")]
+    pub contact_name:  Option<String>,
+    #[serde(rename = "contact_email")]
+    pub contact_email: Value,
+    #[serde(rename = "contact_tax_number")]
+    pub contact_tax_number: Value,
+    #[serde(rename = "contact_phone")]
+    pub contact_phone: Value,
+    #[serde(rename = "contact_address")]
+    pub contact_address: Option<String>,
+    #[serde(rename = "contact_city")]
+    pub contact_city: Value,
+    #[serde(rename = "contact_zip_code")]
+    pub contact_zip_code: Value,
+    #[serde(rename = "contact_state")]
+    pub contact_state: Value,
+    #[serde(rename = "contact_country")]
+    pub contact_country: Value,
+    pub notes: Value,
+    pub attachment: bool,
+    #[serde(rename = "created_from")]
+    pub created_from:  Option<String>,
+    #[serde(rename = "created_by")]
+    pub created_by: Option<i64>,
+    #[serde(rename = "created_at")]
+    pub created_at:  Option<String>,
+    #[serde(rename = "updated_at")]
+    pub updated_at:  Option<String>,
+    pub category: Category,
+    pub currency: Currency,
+    pub contact: Contact,
+    pub histories: Histories,
+    pub items: ListItems,
+    #[serde(rename = "item_taxes")]
+    pub item_taxes: ItemTaxes,
+    pub totals: Totals,
+    pub transactions: Transactions,
+}
+ 
+    
+ 
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Account {
+    pub id: i64,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "type")]
+    pub type_field: String,
+    pub name: String,
+    pub number: String,
+    #[serde(rename = "currency_code")]
+    pub currency_code: String,
+    #[serde(rename = "opening_balance")]
+    pub opening_balance: i64,
+    #[serde(rename = "opening_balance_formatted")]
+    pub opening_balance_formatted: String,
+    #[serde(rename = "current_balance")]
+    pub current_balance: f64,
+    #[serde(rename = "current_balance_formatted")]
+    pub current_balance_formatted: String,
+    #[serde(rename = "bank_name")]
+    pub bank_name: String,
+    #[serde(rename = "bank_phone")]
+    pub bank_phone: Value,
+    #[serde(rename = "bank_address")]
+    pub bank_address: Value,
+    pub enabled: bool,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}  
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListCompanys {
+    pub data: Vec<CompanyData>,
+    pub links: Links,
+    pub meta: Meta,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CompanyData {
+    pub id: i64,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    pub name: String,
+    pub email: String,
+    pub currency: String,
+    pub domain: String,
+    pub address: String,
+    pub logo: String,
+    pub enabled: bool,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: i64,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Links {
+    pub first: String,
+    pub last: String,
+    pub prev: Value,
+    pub next: Value,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Meta {
+    #[serde(rename = "current_page")]
+    pub current_page: i64,
+    pub from: i64,
+    #[serde(rename = "last_page")]
+    pub last_page: i64,
+    pub links: Vec<Link>,
+    pub path: String,
+    #[serde(rename = "per_page")]
+    pub per_page: i64,
+    pub to: i64,
+    pub total: i64,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Link {
+    pub url: Option<String>,
+    pub label: String,
+    pub active: bool,
+}
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InsertInvoiceResult {
+    pub data: InvoiceData,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Category {
+    pub id: Value,
+    #[serde(rename = "company_id")]
+    pub company_id: Value,
+    pub name: String,
+    #[serde(rename = "type")]
+    pub type_field: Value,
+    pub color: Value,
+    pub enabled: Value,
+    #[serde(rename = "parent_id")]
+    pub parent_id: Value,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Currency {
+    pub id: i64,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    pub name: String,
+    pub code: String,
+    pub rate: Option<i64>,
+    pub enabled: bool,
+    pub precision:Option<i64>,
+    pub symbol: String,
+    #[serde(rename = "symbol_first")]
+    pub symbol_first: Option<i64>,
+    #[serde(rename = "decimal_mark")]
+    pub decimal_mark: String,
+    #[serde(rename = "thousands_separator")]
+    pub thousands_separator: String,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Contact {
+    pub id: Value,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    #[serde(rename = "company_id")]
+    pub company_id: Value,
+    #[serde(rename = "user_id")]
+    pub user_id: Value,
+    #[serde(rename = "type")]
+    pub type_field: Value,
+    pub name: String,
+    pub email: Value,
+    #[serde(rename = "tax_number")]
+    pub tax_number: Value,
+    pub phone: Value,
+    pub address: Value,
+    pub website: Value,
+    #[serde(rename = "currency_code")]
+    pub currency_code: Value,
+    pub enabled: Value,
+    pub reference: Value,
+    #[serde(rename = "created_from")]
+    pub created_from: Value,
+    #[serde(rename = "created_by")]
+    pub created_by: Value,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Histories {
+    pub data: Vec<HistoryData>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HistoryData {
+    pub id: i64,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "type")]
+    pub type_field: Option<String>,
+    #[serde(rename = "document_id")]
+    pub document_id: Option<i64>,
+    pub status: Option<String>,
+    pub notify: Option<i64>,
+    pub description: Option<String>,
+    #[serde(rename = "created_from")]
+    pub created_from: Option<String>,
+    #[serde(rename = "created_by")]
+    pub created_by: Option<i64>,
+    #[serde(rename = "created_at")]
+    pub created_at: Option<String>,
+    #[serde(rename = "updated_at")]
+    pub updated_at: Option<String>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ListItems {
+    pub data: Vec<ItemData>,
+}
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetItemData {
+pub id: i64,
+#[serde(rename = "company_id")]
+pub company_id: i64,
+#[serde(rename = "type")]
+pub type_field: String,
+pub name: String,
+pub description: String,
+#[serde(rename = "sale_price")]
+pub sale_price: f64,
+#[serde(rename = "sale_price_formatted")]
+pub sale_price_formatted: String,
+#[serde(rename = "purchase_price")]
+pub purchase_price: f64,
+#[serde(rename = "purchase_price_formatted")]
+pub purchase_price_formatted: String,
+#[serde(rename = "category_id")]
+pub category_id: i64,
+pub picture: bool,
+pub enabled: bool,
+#[serde(rename = "created_from")]
+pub created_from: Value,
+#[serde(rename = "created_by")]
+pub created_by: Value,
+#[serde(rename = "created_at")]
+pub created_at: String,
+#[serde(rename = "updated_at")]
+pub updated_at: String,
+pub taxes: Taxes,
+pub category: Category,
+}
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ItemData {
+    pub id: i64,
+    pub kinbrio_id: Option<uuid::Uuid>,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "type")]
+    pub type_field: Option<String>,
+    #[serde(rename = "document_id")]
+    pub document_id: Option<i64>,
+    #[serde(rename = "item_id")]
+    pub item_id: Option<i64>,
+    pub name: Option<String>,
+    pub description: Option<String>,
+    pub price: Option<f64>,
+    #[serde(rename = "price_formatted")]
+    pub price_formatted: Option<String>,
+    pub total: Option<f64>,
+    #[serde(rename = "total_formatted")]
+    pub total_formatted: Option<String>,
+    #[serde(rename = "created_from")]
+    pub created_from: Option<String>,
+    #[serde(rename = "created_by")]
+    pub created_by: Option<i64>,
+    #[serde(rename = "created_at")]
+    pub created_at: Option<String>,
+    #[serde(rename = "updated_at")]
+    pub updated_at: Option<String>,
+    pub taxes: Taxes,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Taxes {
+    pub data: Vec<Value>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ItemTaxes {
+    pub data: Vec<Value>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Totals {
+    pub data: Vec<TotalData>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TotalData {
+    pub id: i64,
+    #[serde(rename = "company_id")]
+    pub company_id: i64,
+    #[serde(rename = "type")]
+    pub type_field: Option<String>,
+    #[serde(rename = "document_id")]
+    pub document_id: Option<i64>,
+    pub code: Option<String>,
+    pub name: Option<String>,
+    pub amount: f64,
+    #[serde(rename = "amount_formatted")]
+    pub amount_formatted: Option<String>,
+    #[serde(rename = "sort_order")]
+    pub sort_order:Option<i64>,
+    #[serde(rename = "created_from")]
+    pub created_from: Option<String>,
+    #[serde(rename = "created_by")]
+    pub created_by: Option<i64>,
+    #[serde(rename = "created_at")]
+    pub created_at: String,
+    #[serde(rename = "updated_at")]
+    pub updated_at: String,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Transactions {
+    pub data: Vec<Value>,
+}

+ 360 - 0
src/board.rs

@@ -0,0 +1,360 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use uuid::Uuid;
+use strum::IntoEnumIterator;
+
+use tide::{http::mime, Request}; 
+use crate::home::NotFoundTemplate;
+use crate::matrix::post_board_create;
+use crate::task::{TaskStatus, Task, get_tasks_by_organization};
+use crate::{State, user};
+ 
+// SQL STUFF
+
+pub async fn get_organization_boards(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec<Board> {
+    let records = sqlx::query!(
+        "select key, owner_key, organization_key, name, description, columns, lanes, filter, created, updated from boards where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut boards = Vec::<Board>::new();
+    for board in records {
+        let brd = Board {
+            key: board.key.expect("key exists"),
+            organization_key: board.organization_key.expect("organization_key"), 
+            owner_key: board.owner_key.expect("owner_key exists"),
+            name: board.name.expect("name exists"),
+            description: board.description.expect("description exists"), 
+            columns: board.columns.expect("columns"),
+            lanes: board.lanes.expect("columns exists"), 
+            filter: board.filter.expect("filter"),
+            created: board.created.expect("created exists"),
+            updated: board.updated.expect("updated exists"),
+        };
+        boards.push(brd);
+    }
+    return boards
+}
+
+pub async fn get_user_boards(conn: &mut PoolConnection<Postgres>, user_key: uuid::Uuid) -> Vec<Board> {
+    let records = sqlx::query!(
+        "select  key, owner_key, organization_key, name, description, columns, lanes, filter, created, updated from boards where owner_key = $1",
+        user_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut boards = Vec::<Board>::new();
+    for board in records {
+        let brd: Board = Board {
+            key: board.key.expect("key exists"),
+            organization_key: board.organization_key.expect("organization_key"), 
+            owner_key: board.owner_key.expect("owner_key exists"),
+            name: board.name.expect("name exists"),
+            description: board.description.expect("description exists"), 
+            columns: board.columns.expect("columns"),
+            lanes: board.lanes.expect("columns exists"), 
+            filter: board.filter.expect("filter"),
+            created: board.created.expect("created exists"),
+            updated: board.updated.expect("updated exists"),
+        };
+        boards.push(brd);
+    }
+    return boards
+}
+
+pub async fn get_board(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Board {
+    let board = sqlx::query!(
+        "select key, owner_key, organization_key, name, description, columns, lanes, filter, created, updated from boards where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select board by key");
+
+    Board {
+        key: board.key.expect("key exists"),
+        organization_key: board.organization_key.expect("organization_key"), 
+        owner_key: board.owner_key.expect("owner_key exists"),
+        name: board.name.expect("name exists"),
+        description: board.description.expect("description exists"),
+        columns: board.columns.expect("columns exists"), 
+        lanes: board.lanes.expect("columns exists"), 
+        filter: board.filter.expect("filter"),
+        created: board.created.expect("created exists"),
+        updated: board.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_board(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM boards where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+async fn update_board(conn: &mut PoolConnection<Postgres>, board: &Board) {
+    sqlx::query!("UPDATE boards SET name=$1, description=$2, columns=$3, 
+    lanes=$4, filter=$5 where key=$6",  
+    &board.name,
+    &board.description,
+    &board.columns, 
+    &board.lanes, 
+    &board.filter, 
+    board.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+async fn insert_board(conn: &mut PoolConnection<Postgres>, new_board: &Board) {
+    sqlx::query!("INSERT INTO boards (key, organization_key, owner_key, name, description, columns, lanes, filter, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", 
+        new_board.key,
+        new_board.organization_key, 
+        new_board.owner_key,
+        &new_board.name,
+        &new_board.description,
+        &new_board.columns, 
+        &new_board.lanes, 
+        &new_board.filter, 
+        new_board.created,
+        new_board.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    let mut board= Board::new( u.organization_key, u.key,  "".to_owned(), "".to_owned(), vec![], vec![], "".to_owned());
+    board.key = uuid::Uuid::nil();
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+    .content_type(mime::HTML)
+    .body(
+        BoardTemplate::new(
+            board,
+            u,
+            vec![],
+            vec![],
+        )
+        .render_string(),
+    )
+    .build())
+}
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("board_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Board uuid parse");
+            match delete_board(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("board_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Board uuid parse");
+            let board = get_board(&mut conn, s_uuid).await;
+            let tasks = get_tasks_by_organization(&mut conn, u.organization_key).await;
+            let users = crate::user::get_users_by_organization(&mut conn, u.organization_key).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    BoardTemplate::new(
+                        board,
+                        u,
+                        tasks,
+                        users,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    let umd: Result<Board, tide::Error> = req.body_json().await;
+    match umd {
+        Ok(board) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            if board.key == uuid::Uuid::nil() {
+                let s = Board::new(board.organization_key, board.owner_key, board.name, board.description, board.columns, board.lanes, board.filter);
+                insert_board(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_board_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            } else {
+                update_board(&mut conn, &board).await;
+                let j = serde_json::to_string(&board).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+// data types 
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Board {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub name: String,
+    pub description: String,
+    pub columns: Vec::<String>,
+    pub lanes: Vec::<String>,
+    pub filter: String,
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl Board {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        name: String,
+        description: String,
+         columns: Vec::<String>,
+         lanes: Vec::<String>,
+        filter: String,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            owner_key,
+            name,
+            description,
+            columns,
+            lanes,
+            filter,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "board.html")]
+pub struct BoardTemplate {
+    board: Board,
+    user: crate::user::User,
+    tasks: Vec<Task>,
+    users: Vec<crate::user::User>,
+}
+impl<'a> BoardTemplate {
+    pub fn new(
+        board: Board,
+        user: crate::user::User,
+        tasks: Vec<Task>,
+        users: Vec<crate::user::User>,
+    ) -> Self {
+        return Self {
+            board,
+            user,
+            tasks,
+            users,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    } 
+    
+    pub fn lane_name<'aa>(&'aa self, lane: &String) -> String{
+        let st = TaskStatus::from_str(lane).expect("Get status");
+        st.to_string()
+    }
+
+    pub fn get_tasks<'aa>(&'aa self, lane: String, tasks: &'aa Vec<Task>) -> Vec<&Task> {
+        let st = TaskStatus::from_str(&lane).expect("Get status");
+        let lane_tasks = tasks.iter().filter(|t| t.status == st).collect();
+        lane_tasks
+    }
+
+    pub fn lane_contained(&self, lane: &TaskStatus, lanes: &Vec<String>) -> bool {
+        let s_lane = lane.to_string().replace(" ", "");
+        lanes.contains(&s_lane)
+    }
+}

+ 25 - 0
src/common.rs

@@ -0,0 +1,25 @@
+use async_std::{io::{Read, self}, pin::Pin, task::{Context, Poll, ready}};
+
+use async_std::stream::Stream;
+
+#[derive(Debug)]
+pub struct BufferedBytesStream<T> {
+    pub(crate) inner: T,
+}
+
+impl<T: Read + Unpin> Stream for BufferedBytesStream<T> {
+    type Item = async_std::io::Result<Vec<u8>>;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        let mut buf = [0u8; 2048];
+
+        let rd = Pin::new(&mut self.inner);
+
+        match ready!(rd.poll_read(cx, &mut buf)) {
+            Ok(0) => Poll::Ready(None),
+            Ok(n) => Poll::Ready(Some(Ok(buf[..n].to_vec()))),
+            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => Poll::Pending,
+            Err(e) => Poll::Ready(Some(Err(e))),
+        }
+    }
+}

+ 1195 - 0
src/entity.rs

@@ -0,0 +1,1195 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::akaunting::InvoiceData;
+use crate::home::NotFoundTemplate;
+use crate::matrix::{post_entity_create, post_contact_create};
+use crate::{State, file, note, user};
+
+pub async fn get_organization_entitys(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec<Entity> {
+    let records = sqlx::query!(
+        "select 
+        key,
+        organization_key,
+        external_accounting_id,
+        owner_key,
+        name,
+        description, 
+        matrix_room_url, 
+        web_url, 
+        avatar_url, 
+        entity_type, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated from entitys where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select entitys by organization key");
+    let mut entitys = Vec::<Entity>::new();
+    for entity in records {
+        let e_type: EntityType = entity.entity_type.expect("entity_type exists").into();
+        let brd = Entity {
+            key: entity.key.expect("key exists"),
+            organization_key: entity.organization_key.expect("organization_key"), 
+            external_accounting_id: entity.external_accounting_id.expect("external_accounting_id"), 
+            owner_key: entity.owner_key.expect("owner_key exists"),
+            name: entity.name.expect("name exists"),
+            description: entity.description.expect("description exists"),
+            matrix_room_url: entity.matrix_room_url.expect("matrix_room_url exists"),
+            web_url: entity.web_url.expect("web_url exists"),
+            avatar_url: entity.avatar_url.expect("avatar_url exists"), 
+            entity_type: e_type, 
+            address_primary: entity.address_primary.expect("address_primary exists"), 
+            address_unit: entity.address_unit.expect("address_unit exists"), 
+            city: entity.city.expect("city exists"), 
+            state: entity.state.expect("state exists"), 
+            zip_code: entity.zip_code.expect("zip_code exists"), 
+            country: entity.country.expect("country exists"),
+            created: entity.created.expect("created exists"),
+            updated: entity.updated.expect("updated exists"),
+        };
+        entitys.push(brd);
+    }
+    return entitys
+}
+
+pub async fn get_user_entitys(conn: &mut PoolConnection<Postgres>, user_key: uuid::Uuid) -> Vec<Entity> {
+    let records = sqlx::query!(
+        "select 
+        key,
+        organization_key,
+        external_accounting_id,
+        owner_key,
+        name,
+        description, 
+        matrix_room_url, 
+        web_url, 
+        avatar_url, 
+        entity_type, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated from entitys where owner_key = $1",
+        user_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut entitys = Vec::<Entity>::new();
+    for entity in records {
+        let e_type: EntityType = entity.entity_type.expect("entity_type exists").into();
+        let brd: Entity = Entity {
+            key: entity.key.expect("key exists"),
+            external_accounting_id: entity.external_accounting_id.expect("external_accounting_id"), 
+            organization_key: entity.organization_key.expect("organization_key"), 
+            owner_key: entity.owner_key.expect("owner_key exists"),
+            name: entity.name.expect("name exists"),
+            description: entity.description.expect("description exists"),
+            matrix_room_url: entity.matrix_room_url.expect("matrix_room_url exists"),
+            web_url: entity.web_url.expect("web_url exists"),
+            avatar_url: entity.avatar_url.expect("avatar_url exists"), 
+            entity_type: e_type, 
+            address_primary: entity.address_primary.expect("address_primary exists"), 
+            address_unit: entity.address_unit.expect("address_unit exists"), 
+            city: entity.city.expect("city exists"), 
+            state: entity.state.expect("state exists"), 
+            zip_code: entity.zip_code.expect("zip_code exists"), 
+            country: entity.country.expect("country exists"),
+            created: entity.created.expect("created exists"),
+            updated: entity.updated.expect("updated exists"),
+        };
+        entitys.push(brd);
+    }
+    return entitys
+}
+
+pub async fn get_entity(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Entity {
+    let entity = sqlx::query!(
+        "select 
+        key,
+        organization_key,
+        external_accounting_id,
+        owner_key,
+        name,
+        description, 
+        matrix_room_url, 
+        web_url, 
+        avatar_url, 
+        entity_type, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated from entitys where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select entity by key");
+
+    let e_type: EntityType = entity.entity_type.expect("entity_type exists").into();
+    Entity {
+        key: entity.key.expect("key exists"),
+        organization_key: entity.organization_key.expect("organization_key"), 
+        external_accounting_id: entity.external_accounting_id.expect("external_accounting_id"), 
+        owner_key: entity.owner_key.expect("owner_key exists"),
+        name: entity.name.expect("name exists"),
+        description: entity.description.expect("description exists"),
+        matrix_room_url: entity.matrix_room_url.expect("matrix_room_url exists"),
+        web_url: entity.web_url.expect("web_url exists"),
+        avatar_url: entity.avatar_url.expect("avatar_url exists"), 
+        entity_type: e_type, 
+        address_primary: entity.address_primary.expect("address_primary exists"), 
+        address_unit: entity.address_unit.expect("address_unit exists"), 
+        city: entity.city.expect("city exists"), 
+        state: entity.state.expect("state exists"), 
+        zip_code: entity.zip_code.expect("zip_code exists"), 
+        country: entity.country.expect("country exists"),
+        created: entity.created.expect("created exists"),
+        updated: entity.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_entity(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM entitys where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+
+async fn update_entity(conn: &mut PoolConnection<Postgres>, entity: &Entity) {
+    sqlx::query!("UPDATE entitys SET name=$1, description=$2, matrix_room_url=$3, 
+    web_url=$4, avatar_url=$5, entity_type=$6, address_primary=$7, address_unit=$8, 
+    city=$9, state=$10, zip_code=$11, country=$12, external_accounting_id=$13 where key=$14",
+    &entity.name,
+    &entity.description,
+    &entity.matrix_room_url,
+    &entity.web_url,
+    &entity.avatar_url,
+    entity.entity_type as i16,
+    &entity.address_primary,
+    &entity.address_unit,
+    &entity.city,
+    &entity.state,
+    &entity.zip_code,
+    &entity.country,
+    &entity.external_accounting_id,
+    entity.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+pub async fn insert_entity(conn: &mut PoolConnection<Postgres>, new_entity: &Entity) {
+    sqlx::query!("INSERT INTO entitys (
+        key,
+        organization_key,
+        external_accounting_id,
+        owner_key,
+        name,
+        description, 
+        matrix_room_url, 
+        web_url, 
+        avatar_url, 
+        entity_type, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated
+    ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)", 
+        new_entity.key,
+        new_entity.organization_key, 
+        new_entity.external_accounting_id, 
+        new_entity.owner_key,
+        &new_entity.name,
+        &new_entity.description,
+        &new_entity.matrix_room_url,
+        &new_entity.web_url,
+        &new_entity.avatar_url,
+        new_entity.entity_type as i16,
+        &new_entity.address_primary,
+        &new_entity.address_unit,
+        &new_entity.city,
+        &new_entity.state,
+        &new_entity.zip_code,
+        &new_entity.country,
+        new_entity.created,
+        new_entity.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    let mut entity= Entity::new( u.organization_key, "".to_owned(), u.key,  "".to_owned(), "".to_owned(), "".to_owned(),"".to_owned(),"".to_owned(), EntityType::Client,"".to_owned(),"".to_owned(),"".to_owned(),"".to_owned(),"".to_owned(),"".to_owned());
+    entity.key = uuid::Uuid::nil();
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+    .content_type(mime::HTML)
+    .body(
+        EntityTemplate::new(
+            entity,
+            vec![],
+            u,
+            vec![],
+            vec![],
+        )
+        .render_string(),
+    )
+    .build())
+}
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("entity_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Entity uuid parse");
+            match delete_entity(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+
+pub async fn get_invoices(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("entity_id") {
+        Ok(key) => {
+
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Entity uuid parse");
+            let entity = get_entity(&mut conn, s_uuid).await;
+            
+            let options =  crate::akaunting::get_akaunting_options(&mut conn, u.organization_key).await.expect("get options");
+            let invoices = options.list_invoices().await;
+            let accounting_id = match req.param("external_id") {
+                Ok(id)=> id,
+                Err(e) => {
+                    println!("{:?}", e);
+                    return Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                        .content_type(mime::HTML)
+                        .body(NotFoundTemplate::new().render_string())
+                        .build())
+                }
+            };
+            let mut invoice_list = vec![];
+            for i in invoices.data.clone() { 
+                let act_id: i64 = accounting_id.parse().expect("String is i64");
+                if i.contact_id.expect("contact id exists") == act_id {
+                    invoice_list.push(i);
+                }
+            }
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+            .content_type(mime::HTML)
+            .body(
+                InvoiceListTemplate::new(
+                    entity,
+                    invoice_list,
+                    u,
+                )
+                .render_string(),
+            )
+            .build())
+        },
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("entity_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Entity uuid parse");
+            let entity = get_entity(&mut conn, s_uuid).await;
+            let contacts = get_contacts_by_entity(&mut conn, s_uuid).await;
+
+            let notes = note::get_associated_notes(&mut conn, crate::file::AssociationType::Entity, s_uuid).await;
+            let files = file::get_associated_files(&mut conn, crate::file::AssociationType::Entity, s_uuid).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    EntityTemplate::new(
+                        entity,
+                        contacts,
+                        u,
+                        notes,
+                        files,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Entity, tide::Error> = req.body_json().await;
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    match umd {
+        Ok(entity) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            if entity.key == uuid::Uuid::nil() {
+                let s = Entity::new(entity.organization_key, "".to_owned(), entity.owner_key, entity.name, entity.description, entity.matrix_room_url, entity.web_url, entity.avatar_url, entity.entity_type, entity.address_primary, entity.address_unit, entity.city, entity.state, entity.zip_code, entity.country);
+                insert_entity(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_entity_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id,  organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            } else {
+                update_entity(&mut conn, &entity).await;
+                let j = serde_json::to_string(&entity).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+
+pub async fn add_contact(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("entity_id") {
+        Ok(k) => {
+            let entity_uuid = uuid::Uuid::from_str(k).expect("entity key supplied");
+            let mut contact=       Contact::new(
+                entity_uuid,
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                vec![],
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),
+                "".to_owned(),"".to_owned(),
+                "".to_owned(),
+               );
+            contact.key = uuid::Uuid::nil();
+           Ok(tide::Response::builder(tide::StatusCode::Ok)
+           .content_type(mime::HTML)
+           .body(
+               ContactTemplate::new(
+                   contact,
+                   u,
+                   vec!(),
+                   vec!(),
+               )
+               .render_string(),
+           )
+           .build())
+        },
+        Err(_) => todo!(),
+    } 
+}
+
+pub async fn delete_contact(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM contacts where key=$1",  key)
+    .execute(conn)
+    .await;
+} 
+
+pub async fn get_contacts_by_entity(conn: &mut PoolConnection<Postgres>, entity_key: uuid::Uuid) -> Vec::<Contact> {
+    let contact_records = sqlx::query!(
+        "select 
+        c.key,
+        c.external_accounting_id,
+        c.entity_key,
+        c.first_name,
+        c.middle_initial,
+        c.last_name,
+        c.description, 
+        c.position, 
+        c.email, 
+        c.phone, 
+        c.secondary_email, 
+        c.secondary_phone, 
+        c.matrix_user_id,
+        c.web_url, 
+        c.avatar_url, 
+        c.social_urls, 
+        c.address_primary, 
+        c.address_unit, 
+        c.city, 
+        c.state, 
+        c.zip_code, 
+        c.country,
+        c.created, 
+        c.updated from contacts c
+        where c.entity_key = $1",
+        entity_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select contacts by organization_key");
+    let mut contacts = vec![];
+    for contact in contact_records {
+        contacts.push(Contact {
+            key: contact.key.expect("key exists"),
+            external_accounting_id: contact.external_accounting_id.expect("external_accounting_id"), 
+            entity_key: contact.entity_key.expect("entity_key"), 
+            first_name: contact.first_name.expect("first_name exists"),
+            middle_initial: contact.middle_initial.expect("middle_initial exists"),
+            last_name: contact.last_name.expect("last_name exists"),
+            position: contact.position.expect("position exists"),
+            description: contact.description.expect("description exists"),
+            email: contact.email.expect("email exists"),
+            phone: contact.phone.expect("phone exists"),
+            secondary_email: contact.secondary_email.expect("secondary_email exists"),
+            secondary_phone: contact.secondary_phone.expect("secondary_phone exists"),
+            web_url: contact.web_url.expect("web_url exists"),
+            avatar_url: contact.avatar_url.expect("avatar_url exists"), 
+            social_urls: contact.social_urls.expect("social_urls exists"), 
+            matrix_user_id: contact.matrix_user_id.expect("matrix_user_id exists"), 
+            
+            address_primary: contact.address_primary.expect("address_primary exists"), 
+            address_unit: contact.address_unit.expect("address_unit exists"), 
+            city: contact.city.expect("city exists"), 
+            state: contact.state.expect("state exists"), 
+            zip_code: contact.zip_code.expect("zip_code exists"), 
+            country: contact.country.expect("country exists"),
+            created: contact.created.expect("created exists"),
+            updated: contact.updated.expect("updated exists"),
+        });
+    }
+    contacts
+}
+
+pub async fn get_contacts(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec::<Contact> {
+    let contact_records = sqlx::query!(
+        "select 
+        c.key,
+        c.external_accounting_id,
+        c.entity_key,
+        c.first_name,
+        c.middle_initial,
+        c.last_name,
+        c.description, 
+        c.position, 
+        c.email, 
+        c.phone, 
+        c.secondary_email, 
+        c.secondary_phone, 
+        c.matrix_user_id,
+        c.web_url, 
+        c.avatar_url, 
+        c.social_urls, 
+        c.address_primary, 
+        c.address_unit, 
+        c.city, 
+        c.state, 
+        c.zip_code, 
+        c.country,
+        c.created, 
+        c.updated from contacts c
+        inner join entitys e on e.key = c.entity_key 
+        where e.organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select contacts by organization_key");
+    let mut contacts = vec![];
+    for contact in contact_records {
+        contacts.push(Contact {
+            key: contact.key.expect("key exists"),
+            external_accounting_id: contact.external_accounting_id.expect("external_accounting_id"), 
+            entity_key: contact.entity_key.expect("entity_key"), 
+            first_name: contact.first_name.expect("first_name exists"),
+            middle_initial: contact.middle_initial.expect("middle_initial exists"),
+            last_name: contact.last_name.expect("last_name exists"),
+            position: contact.position.expect("position exists"),
+            description: contact.description.expect("description exists"),
+            email: contact.email.expect("email exists"),
+            phone: contact.phone.expect("phone exists"),
+            secondary_email: contact.secondary_email.expect("secondary_email exists"),
+            secondary_phone: contact.secondary_phone.expect("secondary_phone exists"),
+            web_url: contact.web_url.expect("web_url exists"),
+            avatar_url: contact.avatar_url.expect("avatar_url exists"), 
+            social_urls: contact.social_urls.expect("social_urls exists"), 
+            matrix_user_id: contact.matrix_user_id.expect("matrix_user_id exists"), 
+            
+            address_primary: contact.address_primary.expect("address_primary exists"), 
+            address_unit: contact.address_unit.expect("address_unit exists"), 
+            city: contact.city.expect("city exists"), 
+            state: contact.state.expect("state exists"), 
+            zip_code: contact.zip_code.expect("zip_code exists"), 
+            country: contact.country.expect("country exists"),
+            created: contact.created.expect("created exists"),
+            updated: contact.updated.expect("updated exists"),
+        });
+    }
+    contacts
+}
+
+pub async fn get_contact(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Contact {
+    let contact = sqlx::query!(
+        "select 
+        key,
+        external_accounting_id,
+        entity_key,
+        first_name,
+        middle_initial,
+        last_name,
+        description, 
+        position, 
+        email, 
+        phone, 
+        secondary_email, 
+        secondary_phone, 
+        matrix_user_id,
+        web_url, 
+        avatar_url, 
+        social_urls, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated from contacts where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select contact by key");
+
+    Contact {
+        key: contact.key.expect("key exists"),
+        external_accounting_id: contact.external_accounting_id.expect("external_accounting_id"), 
+        entity_key: contact.entity_key.expect("entity_key"), 
+        first_name: contact.first_name.expect("first_name exists"),
+        middle_initial: contact.middle_initial.expect("middle_initial exists"),
+        last_name: contact.last_name.expect("last_name exists"),
+        position: contact.position.expect("position exists"),
+        description: contact.description.expect("description exists"),
+        email: contact.email.expect("email exists"),
+        phone: contact.phone.expect("phone exists"),
+        secondary_email: contact.secondary_email.expect("secondary_email exists"),
+        secondary_phone: contact.secondary_phone.expect("secondary_phone exists"),
+        web_url: contact.web_url.expect("web_url exists"),
+        avatar_url: contact.avatar_url.expect("avatar_url exists"), 
+        social_urls: contact.social_urls.expect("social_urls exists"), 
+        matrix_user_id: contact.matrix_user_id.expect("matrix_user_id exists"), 
+        
+        address_primary: contact.address_primary.expect("address_primary exists"), 
+        address_unit: contact.address_unit.expect("address_unit exists"), 
+        city: contact.city.expect("city exists"), 
+        state: contact.state.expect("state exists"), 
+        zip_code: contact.zip_code.expect("zip_code exists"), 
+        country: contact.country.expect("country exists"),
+        created: contact.created.expect("created exists"),
+        updated: contact.updated.expect("updated exists"),
+    }
+}
+
+async fn update_contact(conn: &mut PoolConnection<Postgres>, contact: &Contact) {
+    let social_urls = contact.social_urls.as_slice();
+    sqlx::query!("UPDATE contacts SET first_name=$1, middle_initial=$2, last_name=$3, 
+    description=$4, position=$5, email=$6, phone=$7, secondary_email=$8, 
+    secondary_phone=$9, matrix_user_id=$10, web_url=$11, avatar_url=$12, social_urls=$13,
+    address_primary=$14, address_unit=$15, city=$16, state=$17, zip_code=$18, country=$19, 
+    external_accounting_id=$20 where key=$21",
+    contact.first_name,
+    &contact.middle_initial,
+    &contact.last_name, 
+    &contact.description,
+    &contact.position, 
+    &contact.email, 
+    &contact.phone, 
+    &contact.secondary_email, 
+    &contact.secondary_phone, 
+    &contact.matrix_user_id, 
+    &contact.web_url, 
+    &contact.avatar_url, 
+    social_urls,
+    &contact.address_primary, 
+    &contact.address_unit, 
+    &contact.city, 
+    &contact.state, 
+    &contact.zip_code, 
+    &contact.country, 
+    &contact.external_accounting_id,
+    contact.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+pub async fn insert_contact(conn: &mut PoolConnection<Postgres>, new_contact: &Contact) {
+    let social_urls = new_contact.social_urls.as_slice();
+    sqlx::query!("INSERT INTO contacts (
+        key,
+        external_accounting_id,
+        entity_key,
+        first_name,
+        middle_initial,
+        last_name,
+        description, 
+        position, 
+        email, 
+        phone, 
+        secondary_email, 
+        secondary_phone, 
+        matrix_user_id,
+        web_url, 
+        avatar_url, 
+        social_urls, 
+        address_primary, 
+        address_unit, 
+        city, 
+        state, 
+        zip_code, 
+        country,
+        created, 
+        updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)", 
+        new_contact.key,
+        new_contact.external_accounting_id,
+        new_contact.entity_key, 
+        new_contact.first_name,
+        &new_contact.middle_initial,
+        &new_contact.last_name, 
+        &new_contact.description,
+        &new_contact.position, 
+        &new_contact.email, 
+        &new_contact.phone, 
+        &new_contact.secondary_email, 
+        &new_contact.secondary_phone, 
+        &new_contact.matrix_user_id, 
+        &new_contact.web_url, 
+        &new_contact.avatar_url, 
+        social_urls,
+        &new_contact.address_primary, 
+        &new_contact.address_unit, 
+        &new_contact.city, 
+        &new_contact.state, 
+        &new_contact.zip_code, 
+        &new_contact.country, 
+        new_contact.created,
+        new_contact.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+
+pub async fn delete_contact_route(req: Request<State>) -> tide::Result {
+    let _u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("contact_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("contact uuid parse");
+            match delete_contact(&mut conn, s_uuid).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get_contact_route(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("contact_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Entity uuid parse");
+            let contact = get_contact(&mut conn, s_uuid).await;
+
+            let notes = note::get_associated_notes(&mut conn, crate::file::AssociationType::Contact, s_uuid).await;
+            let files = file::get_associated_files(&mut conn, crate::file::AssociationType::Contact, s_uuid).await;
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    ContactTemplate::new(
+                        contact,
+                        u,
+                        notes,
+                        files,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert_contact_route(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Contact, tide::Error> = req.body_json().await;
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    
+    match umd {
+        Ok(contact) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            
+            if contact.key == uuid::Uuid::nil() {
+                let s = Contact::new(
+                    contact.entity_key,
+                    contact.external_accounting_id,
+                    contact.first_name, 
+                    contact.middle_initial, 
+                    contact.last_name, 
+                    contact.description, 
+                    contact.position, 
+                    contact.email, 
+                    contact.phone, 
+                    contact.secondary_email, 
+                    contact.secondary_phone, 
+                    contact.matrix_user_id, 
+                    contact.web_url, 
+                    contact.avatar_url, 
+                    contact.social_urls, 
+                    contact.address_primary, 
+                    contact.address_unit, 
+                    contact.city, 
+                    contact.state, 
+                    contact.zip_code, 
+                    contact.country,
+                    );
+                insert_contact(&mut conn, &s).await;
+                post_contact_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, uuid::Uuid::from_str(claims.organization_key.as_str()).expect("org key"), claims.matrix_access_token,  &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())   
+            }
+            update_contact(&mut conn, &contact).await;
+            let j = serde_json::to_string(&contact).expect("To JSON");
+            return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type)]
+pub enum EntityType {
+    Client,
+    Supplier,
+}
+
+impl Into<EntityType> for i16 {
+    fn into(self) -> EntityType {
+        match self {
+            0 => EntityType::Client,
+            1 => EntityType::Supplier,
+            _ => EntityType::Client
+        }
+    }
+}
+
+impl From<EntityType> for i16 {
+    fn from(t: EntityType) -> Self {
+        match t {
+            EntityType::Client => 0,
+            EntityType::Supplier => 1 
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Entity {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub external_accounting_id: String,
+    pub owner_key: uuid::Uuid,
+    pub matrix_room_url: String,
+    pub web_url: String,
+    pub avatar_url: String,
+    pub entity_type: EntityType, 
+    pub name: String,
+    pub description: String, 
+
+    pub address_primary:String,
+    pub address_unit: String,
+    pub city: String,
+    pub state: String,
+    pub zip_code: String,
+    pub country: String,
+
+    pub created: i64,
+    pub updated: i64,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Contact {
+    pub key: uuid::Uuid,
+    pub external_accounting_id: String,
+    pub entity_key: uuid::Uuid,
+    pub first_name: String,
+    pub middle_initial: String,
+    pub last_name: String,
+    pub description: String,
+    pub position: String, 
+    pub email: String,
+    pub phone: String,
+    pub secondary_email: String,
+    pub secondary_phone: String,
+    pub matrix_user_id: String,
+    pub web_url: String,
+    pub avatar_url: String,
+    pub social_urls: Vec<String>,
+    pub address_primary:String,
+    pub address_unit: String,
+    pub city: String,
+    pub state: String,
+    pub zip_code: String,
+    pub country: String,
+    pub created: i64,
+    pub updated: i64,
+}
+
+impl Contact {
+    pub fn new(
+            entity_key:Uuid,
+            external_accounting_id: String,
+            first_name:String,
+            middle_initial:String,
+            last_name:String,
+            description:String,
+            position:String,
+            email:String,
+            phone:String,
+            secondary_email:String,
+            secondary_phone:String,
+            matrix_user_id:String,
+            web_url:String,
+            avatar_url:String,
+            social_urls:Vec<String>,
+            address_primary:String,
+            address_unit:String,
+            city:String,
+            state:String,
+            zip_code:String,
+            country:String,
+    ) -> Contact {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            entity_key,
+            external_accounting_id,
+            first_name,
+            middle_initial,
+            last_name,
+            description,
+            position,
+            email,
+            phone,
+            secondary_email,
+            secondary_phone,
+            matrix_user_id,
+            web_url,
+            avatar_url,
+            social_urls,
+            address_primary,
+            address_unit,
+            city,
+            state,
+            zip_code,
+            country,
+            created,
+            updated,
+        }
+    }
+}
+
+impl Entity {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        external_accounting_id: String,
+        owner_key: uuid::Uuid,
+        name: String,
+        description: String, 
+        matrix_room_url: String, 
+        web_url: String, 
+        avatar_url: String, 
+        entity_type: EntityType, 
+        address_primary: String, 
+        address_unit: String, 
+        city: String, 
+        state: String, 
+        zip_code: String, 
+        country: String,
+    ) -> Entity {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            external_accounting_id,
+            owner_key,
+            name,
+            description, 
+            created, 
+            updated,
+            matrix_room_url, 
+            web_url, 
+            avatar_url, 
+            entity_type, 
+            address_primary, 
+            address_unit, 
+            city, 
+            state, 
+            zip_code, 
+            country, 
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "invoice_list.html")]
+pub struct InvoiceListTemplate {
+    entity: Entity,
+    invoices: Vec<InvoiceData>,
+    user: crate::user::User,
+}
+
+impl<'a> InvoiceListTemplate {
+    pub fn new(
+        entity: Entity,
+        invoices: Vec<InvoiceData>,
+        user: crate::user::User,
+    ) -> Self {
+        return Self {
+            entity,
+            invoices,
+            user,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "entity.html")]
+pub struct EntityTemplate {
+    entity: Entity,
+    contacts: Vec::<Contact>,
+    user: crate::user::User,
+    notes: Vec<note::Note>,
+    files: Vec<file::File>,
+}
+impl<'a> EntityTemplate {
+    pub fn new(
+        entity: Entity,
+        contacts: Vec::<Contact>,
+        user: crate::user::User,
+        notes: Vec<note::Note>,
+        files: Vec<file::File>,
+    ) -> Self {
+        return Self {
+            entity,
+            contacts,
+            user,
+            notes,
+            files,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "contact.html")]
+pub struct ContactTemplate {
+    contact: Contact,
+    user: crate::user::User,
+    notes: Vec<note::Note>,
+    files: Vec<file::File>,
+}
+
+impl<'a> ContactTemplate {
+    pub fn new(
+        contact: Contact,
+        user: crate::user::User,
+        notes: Vec<note::Note>,
+        files: Vec<file::File>,
+    ) -> Self {
+        return Self {
+            contact,
+            user,
+            notes,
+            files,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 584 - 0
src/file.rs

@@ -0,0 +1,584 @@
+use std::io::Cursor;
+use std::str::FromStr;
+
+use crate::common::BufferedBytesStream;
+use askama::Template;
+use minio_rsc::Minio;
+use minio_rsc::provider::StaticProvider;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::postgres::PgQueryResult;
+use sqlx::Postgres;
+use tokio::io::AsyncWriteExt;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::matrix::post_file_create;
+use crate::{user, State};
+
+
+pub async fn get_associated_files(
+    conn: &mut PoolConnection<Postgres>,
+    association_type: AssociationType,
+    association_key: uuid::Uuid,
+) -> Vec<File> {
+    let file_records = sqlx::query!(
+        "select key, owner_key, organization_key, association_type, association_key, url, hash, name, description, tags, format, size, created, updated 
+        from files 
+        WHERE association_type = $1 AND association_key = $2",
+        association_type as i16, association_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select file by association key");
+    let mut files = vec![];
+    for file in file_records {
+        let association_type: AssociationType = file
+            .association_type
+            .expect("association_type exists")
+            .into();
+        files.push(File {
+            key: file.key.expect("key exists"),
+            owner_key: file.owner_key.expect("owner_key exists"),
+            organization_key: file.organization_key.expect("organization_key exists"),
+            hash: file.hash.expect("hash exists"),
+            name: file.name.expect("name exists"),
+            description: file.description.expect("description exists"),
+            url: file.url.expect("url exists"),
+            tags: file.tags.expect("tags exists"),
+            format: file.format.expect("format exists"),
+            size: file.size.expect("size exists"),
+            created: file.created.expect("created exists"),
+            updated: file.updated.expect("updated exists"),
+            association_type,
+            association_key: file.association_key.expect("association_key exists"),
+        });
+    }
+    return files;
+}
+ 
+
+pub async fn get_organization_files(
+    conn: &mut PoolConnection<Postgres>,
+    org_key: uuid::Uuid,
+) -> Vec<File> {
+    let file_records = sqlx::query!(
+        "select key, owner_key, organization_key, association_type, association_key, url, hash, name, description, tags, format, size, created, updated from files where organization_key = $1",
+        org_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select file by key");
+    let mut files = vec![];
+    for file in file_records {
+        let association_type: AssociationType = file
+            .association_type
+            .expect("association_type exists")
+            .into();
+        files.push(File {
+            key: file.key.expect("key exists"),
+            owner_key: file.owner_key.expect("owner_key exists"),
+            organization_key: file.organization_key.expect("organization_key exists"),
+            hash: file.hash.expect("hash exists"),
+            name: file.name.expect("name exists"),
+            description: file.description.expect("description exists"),
+            url: file.url.expect("url exists"),
+            tags: file.tags.expect("tags exists"),
+            format: file.format.expect("format exists"),
+            size: file.size.expect("size exists"),
+            created: file.created.expect("created exists"),
+            updated: file.updated.expect("updated exists"),
+            association_type,
+            association_key: file.association_key.expect("association_key exists"),
+        });
+    }
+    return files;
+}
+
+pub async fn get_file(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> File {
+    let file = sqlx::query!(
+        "select key, owner_key, organization_key, association_type, association_key, url, hash, name, description, tags, format, size, created, updated from files where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select file by key");
+
+    let association_type: AssociationType = file
+        .association_type
+        .expect("association_type exists")
+        .into();
+    File {
+        key: file.key.expect("key exists"),
+        owner_key: file.owner_key.expect("owner_key exists"),
+        organization_key: file.organization_key.expect("organization_key exists"),
+        hash: file.hash.expect("hash exists"),
+        name: file.name.expect("name exists"),
+        description: file.description.expect("description exists"),
+        url: file.url.expect("url exists"),
+        tags: file.tags.expect("tags exists"),
+        format: file.format.expect("format exists"),
+        size: file.size.expect("size exists"),
+        created: file.created.expect("created exists"),
+        updated: file.updated.expect("updated exists"),
+        association_type,
+        association_key: file.association_key.expect("association_key exists"),
+    }
+}
+
+async fn delete_file(
+    conn: &mut PoolConnection<Postgres>,
+    key: uuid::Uuid,
+    owner_key: uuid::Uuid,
+) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!(
+        "DELETE FROM files where owner_key=$1 AND key=$2",
+        owner_key,
+        key
+    )
+    .execute(conn)
+    .await;
+}
+
+async fn insert_file(conn: &mut PoolConnection<Postgres>, new_file: &File) {
+    sqlx::query!("INSERT INTO files (key, owner_key, organization_key, association_type, association_key, url, hash, name, description, tags, format, size, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", 
+        new_file.key,
+        new_file.owner_key,
+        new_file.organization_key,
+        new_file.association_type as i16,
+        new_file.association_key,
+        &new_file.url,
+        &new_file.hash,
+        &new_file.name,
+        &new_file.description,
+        &new_file.tags,
+        &new_file.format,
+        new_file.size,
+        new_file.created,
+        new_file.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("file_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("File uuid parse");
+            match delete_file(&mut conn, s_uuid, u.key).await {
+                Ok(_) => Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::HTML)
+                    .build()),
+                Err(_) => Ok(
+                    tide::Response::builder(tide::StatusCode::InternalServerError)
+                        .content_type(mime::HTML)
+                        .body(NotFoundTemplate::new().render_string())
+                        .build(),
+                ),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+
+    match req.param("association_type") {
+        Ok(association_type) => match req.param("association_id") {
+            Ok(association_id) => {
+                let association_id = uuid::Uuid::from_str(association_id).expect("file uuid parse");
+                let mut file = File::new(
+                    u.key,
+                    u.organization_key,
+                    AssociationType::from_str(association_type).expect("Valid association type"),
+                    association_id,
+                    "".to_owned(),
+                    "".to_owned(),
+                    "".to_owned(),
+                    "".to_owned(),
+                    "".to_owned(),
+                    "".to_owned(),
+                    0,
+                );
+                file.key = uuid::Uuid::nil();
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::HTML)
+                    .body(FileTemplate::new(file, u).render_string())
+                    .build())
+            }
+            Err(e) => {
+                println!("{:?}", e);
+                Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                    .content_type(mime::HTML)
+                    .body(NotFoundTemplate::new().render_string())
+                    .build())
+            }
+        },
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    match req.param("file_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("File uuid parse");
+            let file = get_file(&mut conn, s_uuid).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(FileTemplate::new(file, u).render_string())
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get_file_fs(association_type: String, association_key: String, name: String) -> Result<reqwest::Response, minio_rsc::error::Error> {
+    let bucket_name = get_bucket_name(association_type, association_key);
+
+    let s3_url = std::env::var("S3_URL")
+    .expect("Missing `S3_URL` env variable, needed for running the server");
+    let static_provider = StaticProvider::new(
+        "Y8oJ5TXXk659r0a8Hxlh",
+        "J0S5VgsVbI9u77Jtwnihht1ZkWn1OTKAvGEpptBr",
+        None,
+    );
+    let minio_client = Minio::builder()
+    .endpoint(s3_url)
+    .provider(static_provider)
+    .secure(false)
+    .build()
+    .unwrap();
+    minio_client.get_object(bucket_name, name).await
+}
+
+pub async fn insert(req: Request<State>) -> tide::Result {
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    let mime = req.content_type().unwrap();
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            println!("DB ERROR");
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    
+    if mime.essence().to_string() == "multipart/form-data" {
+        let boundary = mime.param("boundary").unwrap().to_string();
+        let mut body = BufferedBytesStream { inner: req };
+        let mut multipart = multer::Multipart::new(&mut body, boundary);
+        let mut key = "".to_string();
+        let mut name = "".to_string();
+        let mut description = "".to_string();
+        let mut format = "".to_string();
+        let mut tags = "".to_string();
+        let mut url = "".to_string();
+        let mut organization_key = "".to_string();
+        let mut association_type = "".to_string();
+        let mut association_key = "".to_string();
+        let mut hash = "".to_string();
+        let mut size = 0;
+        
+        let mut buffer = vec![];
+        while let Some(mut field) = multipart.next_field().await.expect("next field") {
+            let f_name = field.name().clone().expect("get field name");
+            println!("{f_name}");
+            if f_name == "name" {
+                name = field.text().await.expect("name multi field");
+            } else if f_name == "key" {
+                key = field.text().await.expect("key multi field");
+            } else if f_name == "description" {
+                description = field.text().await.expect("description multi field");
+            } else if f_name == "tags" {
+                tags = field.text().await.expect("tags field");
+            } else if f_name == "organization_key" {
+                organization_key = field.text().await.expect("organization_key multi field");
+            } else if f_name == "association_type" {
+                association_type = field.text().await.expect("association_type multi field");
+            } else if f_name == "association_key" {
+                association_key = field.text().await.expect("association_key multi field");
+            } else if f_name == "hash" {
+                hash = field.text().await.expect("hash multi field");
+            } else if f_name == "size" {
+                size = field.text().await.expect("size multi field").parse::<i64>().expect("Valid int64");
+            } else if f_name == "url" {
+                url = field.text().await.expect("url multi field");
+            } else if f_name == "format" {
+                format = field.text().await.expect("format field");
+            } else if f_name == "file" {
+                while let Some(chunk) = field
+                    .chunk()
+                    .await
+                    .expect("read in chunk from multipart stream")
+                {
+                    buffer.write_all(&chunk).await.expect("Write to s3buffer");
+                }
+            }
+        }
+        let s3_url = std::env::var("S3_URL")
+        .expect("Missing `S3_URL` env variable, needed for running the server");
+        let static_provider = StaticProvider::new(
+            "Y8oJ5TXXk659r0a8Hxlh",
+            "J0S5VgsVbI9u77Jtwnihht1ZkWn1OTKAvGEpptBr",
+            None,
+        );
+        let minio_client = Minio::builder()
+        .endpoint(s3_url)
+        .provider(static_provider)
+        .secure(false)
+        .build()
+        .unwrap();
+      
+        let bucket_name = get_bucket_name(association_type.clone(), association_key.clone());
+        minio_client.make_bucket(bucket_name.clone(), true).await.unwrap_or_else(|_e| "Already Exists".to_string());
+        minio_client.put_object(bucket_name, name.clone(), buffer.into()).await.expect("Put buffer");
+        let mut s = File::new(
+            uuid::Uuid::from_str(claims.key.as_str()).expect("user key exists"),
+            uuid::Uuid::from_str(organization_key.as_str()).expect("organization_key exists"),
+            AssociationType::from_str(association_type.as_str()).expect("Valid association type"),
+            uuid::Uuid::from_str(association_key.as_str()).expect("association_key exists"),
+            url,
+            hash,
+            name.clone(),
+            description,
+            tags,
+            format,
+            size,
+        );
+        let key = uuid::Uuid::from_str(key.as_str()).expect("user key exists");
+        if !key.is_nil() {
+            s.key = key;
+            // updo0t
+        } else {
+            insert_file(&mut conn, &s).await;
+            let organization_key =
+                uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+            post_file_create(
+                &mut conn,
+                claims.matrix_home_server,
+                claims.matrix_user_id,
+                organization_key, 
+                claims.matrix_access_token,
+                &s,
+            )
+            .await
+            .expect("Posting to matrix");
+        }
+        return Ok(tide::Redirect::new("/").into());
+    }
+    Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+        .content_type(mime::JSON)
+        .body("{'error': 'invalid multipart body'}")
+        .build())
+}
+
+fn get_bucket_name(association_type: String, association_key:String) -> String {
+    format!("{}-{}-fs", association_type.to_lowercase(), association_key.to_lowercase())
+}
+// data types
+
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type)]
+pub enum AssociationType {
+    Organization,
+    Project,
+    Task,
+    Entity,
+    Contact,
+    Milestone,
+    User,
+}
+
+impl FromStr for AssociationType {
+    type Err = ();
+    fn from_str(input: &str) -> Result<AssociationType, Self::Err> {
+        match input {
+            "Organization" => Ok(AssociationType::Organization),
+            "Project" => Ok(AssociationType::Project),
+            "Task" => Ok(AssociationType::Task),
+            "Entity" => Ok(AssociationType::Entity),
+            "Contact" => Ok(AssociationType::Contact),
+            "Milestone" => Ok(AssociationType::Milestone),
+            "User" => Ok(AssociationType::User),
+            _ => Ok(AssociationType::Organization),
+        }
+    }
+}
+
+impl ToString for AssociationType {
+    fn to_string(&self) -> String {
+        match self {
+            AssociationType::Organization => "Organization".to_owned(),
+            AssociationType::Project => "Project".to_owned(),
+            AssociationType::Task => "Task".to_owned(),
+            AssociationType::Entity => "Entity".to_owned(),
+            AssociationType::Contact => "Contact".to_owned(),
+            AssociationType::Milestone => "Milestone".to_owned(),
+            AssociationType::User => "User".to_owned(),
+        }
+    }
+}
+
+impl Into<AssociationType> for i16 {
+    fn into(self) -> AssociationType {
+        match self {
+            0 => AssociationType::Organization,
+            1 => AssociationType::Project,
+            2 => AssociationType::Task,
+            3 => AssociationType::Entity,
+            4 => AssociationType::Contact,
+            5 => AssociationType::Milestone,
+            6 => AssociationType::User,
+            _ => AssociationType::Organization,
+        }
+    }
+}
+
+impl From<AssociationType> for i16 {
+    fn from(t: AssociationType) -> Self {
+        match t {
+            AssociationType::Organization => 0,
+            AssociationType::Project => 1,
+            AssociationType::Task => 2,
+            AssociationType::Entity => 3,
+            AssociationType::Contact => 4,
+            AssociationType::Milestone => 5,
+            AssociationType::User => 6,
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct File {
+    pub key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub association_type: AssociationType,
+    pub association_key: uuid::Uuid,
+    pub url: String,
+    pub hash: String,
+    pub name: String,
+    pub description: String,
+    pub tags: String,
+    pub format: String,
+    pub size: i64,
+    pub created: i64,
+    pub updated: i64,
+}
+
+impl File {
+    pub fn new(
+        owner_key: uuid::Uuid,
+        organization_key: uuid::Uuid,
+        association_type: AssociationType,
+        association_key: uuid::Uuid,
+        url: String,
+        hash: String,
+        name: String,
+        description: String,
+        tags: String,
+        format: String,
+        size: i64,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            owner_key,
+            organization_key,
+            association_type,
+            association_key,
+            url,
+            hash,
+            name,
+            description,
+            tags,
+            format,
+            size,
+            created,
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "file.html")]
+pub struct FileTemplate {
+    file: File,
+    user: crate::user::User,
+}
+
+impl<'a> FileTemplate {
+    pub fn new(file: File, user: crate::user::User) -> Self {
+        return Self { file, user };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+#[derive(Template)]
+#[template(path = "xls_editor.html")]
+pub struct XLSEditorTemplate {
+    file: File,
+    user: crate::user::User,
+}
+
+impl<'a> XLSEditorTemplate {
+    pub fn new(file: File, user: crate::user::User) -> Self {
+        return Self { file, user };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 244 - 0
src/home.rs

@@ -0,0 +1,244 @@
+use std::str::FromStr;
+
+use askama::Template;
+
+use crate::{
+    board, entity, organization, project,
+    user::{self, User},
+    State, file, note, service_item,
+};
+use tide::{http::mime, Request};
+
+pub async fn home(req: Request<State>) -> tide::Result {
+    return match user::read_jwt_cookie(req.cookie("token")) {
+        Some(_c) => dashboard(req).await,
+        None => {
+            let home = HomeTemplate::new(user::User::new(
+                uuid::Uuid::nil(),
+                uuid::Uuid::nil(),
+                "".to_string(),
+                "".to_string(),
+                "".to_string(),
+            ));
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(home.render_string())
+                .build())
+        }
+    };
+}
+pub async fn account(req: Request<State>) -> tide::Result {
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    let key = uuid::Uuid::from_str(claims.key.as_str()).expect("uuid parse");
+    let user = user::get_user(&mut conn, key).await.expect("user exists");
+
+    let home = user::UserTemplate::new(&user, user.email.as_str());
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(home.render_string())
+        .build())
+}
+
+pub async fn documentation(req: Request<State>) -> tide::Result {
+    return Ok(tide::Redirect::new("http://localhost:3005").into());
+    let user = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => user::User::new(
+            uuid::Uuid::from_str(c.key.as_str()).expect("parsed cookie"),
+            uuid::Uuid::from_str(c.organization_key.as_str()).expect("parsed cookie"),
+            c.email,
+            c.matrix_user_id,
+            c.matrix_home_server,
+        ),
+        None => {
+             user::User::new(
+                uuid::Uuid::nil(),
+                uuid::Uuid::nil(),
+                "".to_string(),
+                "".to_string(),
+                "".to_string(),
+             )
+        }
+    };
+    
+    let docs = DocumentationTemplate::new(user);
+    return Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(docs.render_string())
+        .build());
+}
+
+pub async fn dashboard(req: Request<State>) -> tide::Result {
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    let key = uuid::Uuid::from_str(claims.key.as_str()).expect("uuid parse");
+    let user = match user::get_user(&mut conn, key).await {
+        Some(u) => u,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    let org_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("Valid UUID");
+    
+    let projects = project::get_projects_by_organization(&mut conn, org_key).await;
+    let organization = organization::get_organization(&mut conn, org_key).await;
+    let organization_boards = board::get_organization_boards(&mut conn, org_key).await;
+    let user_boards = board::get_user_boards(&mut conn, key).await;
+    let entitys = entity::get_organization_entitys(&mut conn, org_key).await;
+    let contacts = entity::get_contacts(&mut conn, org_key).await;
+    let notes = note::get_organization_notes(&mut conn, org_key).await;
+    let files = file::get_organization_files(&mut conn, org_key).await;
+    let service_items = service_item::get_organization_service_items(&mut conn, org_key).await;
+    let home = DashboardTemplate::new(
+        user,
+        organization,
+        projects,
+        organization_boards,
+        user_boards,
+        service_items,
+        entitys,
+        contacts,
+        notes,
+        files,
+    );
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(home.render_string())
+        .build())
+}
+
+#[derive(Template)]
+#[template(path = "documentation.html")]
+pub struct DocumentationTemplate {
+    user: User,
+}
+
+impl<'a> DocumentationTemplate {
+    pub fn new(user: User) -> Self {
+        return Self { user };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "dashboard.html")]
+pub struct DashboardTemplate {
+    user: User,
+    organization: organization::Organization,
+    projects: Vec<project::Project>,
+    organization_boards: Vec<board::Board>,
+    user_boards: Vec<board::Board>,
+    service_items: Vec<service_item::ServiceItem>,
+    entitys: Vec<entity::Entity>,
+    contacts: Vec<entity::Contact>,
+    notes: Vec<note::Note>,
+    files: Vec<file::File>,
+}
+
+impl<'a> DashboardTemplate {
+    pub fn new(
+        user: User,
+        organization: organization::Organization,
+        projects: Vec<project::Project>,
+        organization_boards: Vec<board::Board>,
+        user_boards: Vec<board::Board>,
+        service_items: Vec<service_item::ServiceItem>,
+        entitys: Vec<entity::Entity>,
+        contacts: Vec<entity::Contact>,
+        notes: Vec<note::Note>,
+        files: Vec<file::File>,
+    ) -> Self {
+        return Self {
+            user,
+            organization,
+            projects,
+            organization_boards,
+            user_boards,
+            service_items,
+            entitys,
+            contacts,
+            notes,
+            files,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "home.html")]
+pub struct HomeTemplate {
+    user: User,
+}
+
+impl HomeTemplate {
+    pub fn new(user: User) -> Self {
+        return Self { user };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "notfound.html")]
+pub struct NotFoundTemplate {}
+
+impl<'a> NotFoundTemplate {
+    pub fn new() -> Self {
+        return Self {};
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 204 - 0
src/main.rs

@@ -0,0 +1,204 @@
+mod akaunting;
+mod board;
+mod common;
+mod entity;
+mod file;
+mod home;
+mod matrix;
+mod milestone;
+mod note;
+mod organization;
+mod project;
+mod service_item;
+mod task;
+mod user;
+use dotenv::dotenv;
+use file::get_file_fs;
+use sqlx::{PgPool, Pool};
+use tide::{Body, Request, Response, StatusCode, http::mime};
+use tokio::io;
+use user::{read_jwt_cookie, UserJwtState};
+#[derive(Clone, Debug)]
+pub struct State {
+    db_pool: PgPool,
+}
+
+pub async fn serve_dir(req: Request<State>) -> tide::Result {
+    let f_local_path = "./assets".to_string() + req.url().path().replace("/fs", "").as_str();
+    match Body::from_file(f_local_path.clone()).await {
+        Ok(body) => {
+            let mut builder = Response::builder(StatusCode::Ok).body(body);
+            if f_local_path.clone().contains(".css") || f_local_path.contains(".js") {
+                builder = builder.header("Cache-Control", "max-age=31536000, immutable");
+            }
+            Ok(builder.build())
+        }
+        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Response::new(StatusCode::NotFound)),
+        Err(e) => Err(e.into()),
+    }
+}
+
+pub async fn serve_s3(req: Request<State>) -> tide::Result {
+    let claims: UserJwtState = match read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(Response::new(StatusCode::Unauthorized));
+        }
+    };
+    let format = match req.param("format") {
+        Ok(i) => i,
+        Err(e) => return Ok(Response::new(StatusCode::BadRequest)),
+    };
+    let association_type = match req.param("association_type") {
+        Ok(i) => i,
+        Err(e) => return Ok(Response::new(StatusCode::BadRequest)),
+    };
+    let association_id = match req.param("association_id") {
+        Ok(i) => i,
+        Err(e) => return Ok(Response::new(StatusCode::BadRequest)),
+    };
+    let name = match req.param("name") {
+        Ok(i) => i,
+        Err(e) => return Ok(Response::new(StatusCode::BadRequest)),
+    };
+    let organization_id = match req.param("organization_id") {
+        Ok(i) => i,
+        Err(e) => return Ok(Response::new(StatusCode::BadRequest)),
+    };
+    let org_claim = claims.organization_key.clone();
+    println!("{organization_id} {org_claim}");
+    if organization_id != claims.organization_key {
+        return Ok(Response::new(StatusCode::Unauthorized));
+    }
+    let resp = get_file_fs(association_type.to_owned(), association_id.to_owned(), name.to_owned()).await.expect("S3 worked");
+    let m_type = match format {
+        "jpg" => mime::JPEG,
+        "jpeg" => mime::JPEG,
+        "png" => mime::PNG,
+        "svg" => mime::SVG,
+        "ico" => mime::ICO,
+        _ => mime::BYTE_STREAM,
+    };
+    let body = Body::from_bytes(resp.bytes().await.expect("Body").to_vec());
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(m_type)
+        .body(body)
+        .build())
+}
+
+// me fucking aroud making a macro that looks like log!("msg") and then does println!... it's not so spooky afterall
+macro_rules! log {
+    // `()` indicates that the macro takes no argument.
+    ($msg:literal) => {
+        // The macro will expand into the contents of this block.
+        println!($msg)
+    };
+}
+
+#[tokio::main]
+async fn main() -> tide::Result<()> {
+    dotenv().ok();
+    let db_url = std::env::var("DATABASE_URL")
+        .expect("Missing `DATABASE_URL` env variable, needed for running the server");
+
+    let db_pool: PgPool = Pool::connect(&db_url).await.unwrap();
+
+    let state = State { db_pool };
+    let mut app = tide::with_state(state);
+    app.with(tide_compress::CompressMiddleware::new());
+
+    app.at("/").get(home::home);
+
+    app.at("/dashboard").get(home::dashboard);
+    app.at("/documentation").get(home::documentation);
+    app.at("/account").get(home::account);
+
+    app.at("/login").get(user::login);
+    app.at("/logout").get(user::logout);
+    app.at("/login").post(user::login_post);
+    app.at("/login_matrix").get(user::login_matrix);
+    app.at("/login_by_username").get(user::login_by_username);
+
+    app.at("/register").get(user::register);
+    app.at("/register").post(user::register_post);
+    app.at("/register_matrix").get(user::register_post);
+
+    app.at("/users/:user_id").post(user::update);
+    app.at("/users/:user_id").get(user::get);
+    app.at("/users/:user_id").delete(user::delete);
+
+    app.at("/project").post(project::insert);
+    app.at("/project/add").get(project::add);
+    app.at("/project/:project_id").get(project::get);
+    app.at("/project/:project_id").delete(project::delete);
+
+    app.at("/task").post(task::insert);
+    app.at("/task/add/:project_id").get(task::add);
+    app.at("/task/:task_id").get(task::get);
+    app.at("/task/:task_id").delete(task::delete);
+
+    app.at("/entity").post(entity::insert);
+    app.at("/entity/add").get(entity::add);
+    app.at("/entity/:entity_id").get(entity::get);
+    app.at("/entity/:entity_id").delete(entity::delete);
+    app.at("/entity/invoices/:entity_id/:external_id")
+        .get(entity::get_invoices);
+
+    app.at("/contact").post(entity::insert_contact_route);
+    app.at("/contact/add/:entity_id").get(entity::add_contact);
+    app.at("/contact/:contact_id")
+        .get(entity::get_contact_route);
+    app.at("/contact/:contact_id")
+        .delete(entity::delete_contact_route);
+
+    app.at("/board").post(board::insert);
+    app.at("/board/add").get(board::add);
+    app.at("/board/:board_id").get(board::get);
+    app.at("/board/:board_id").delete(board::delete);
+
+    app.at("/service_item").post(service_item::insert);
+    app.at("/service_item/add").get(service_item::add);
+    app.at("/service_item/:service_item_id")
+        .get(service_item::get);
+    app.at("/service_item/:service_item_id")
+        .delete(service_item::delete);
+
+    app.at("/organization").post(organization::insert);
+    app.at("/organization/:organization_id")
+        .get(organization::get);
+    app.at("/organization/:organization_id")
+        .delete(organization::delete);
+
+    app.at("/milestone").post(milestone::insert);
+    app.at("/milestone/add/:project_id").get(milestone::add);
+    app.at("/milestone/:milestone_id").get(milestone::get);
+    app.at("/milestone/:milestone_id").delete(milestone::delete);
+
+    app.at("/akaunting").post(akaunting::save_akaunting_options);
+    app.at("/akaunting")
+        .get(akaunting::get_akaunting_options_page);
+    app.at("/akaunting/import_item")
+        .post(akaunting::import_item);
+    app.at("/akaunting/import_customer")
+        .post(akaunting::import_customer);
+
+    app.at("/file").post(file::insert);
+    app.at("/file/add/:association_type/:association_id")
+        .get(file::add);
+    app.at("/file/:file_id").get(file::get);
+    app.at("/file/:file_id").delete(file::delete);
+
+    app.at("/note").post(note::insert);
+    app.at("/note/add/:association_type/:association_id")
+        .get(note::add);
+    app.at("/note/:note_id").get(note::get);
+    app.at("/note/:note_id").delete(note::delete);
+
+    app.at("/fs/*").get(serve_dir);
+    app.at("/files/:format/:organization_id/:association_type/:association_id/:name")
+        .get(serve_s3);
+    // app.at("/fs").serve_dir("./assets")?;
+
+    app.listen("0.0.0.0:8080").await?;
+    Ok(())
+}

+ 712 - 0
src/matrix.rs

@@ -0,0 +1,712 @@
+use std::fmt;
+
+use matrix_sdk::{
+    self,
+    config::SyncSettings,
+    ruma::{
+        api::client::session::{
+            get_login_types::v3::{IdentityProvider, LoginType},
+            login,
+        },
+        device_id,
+        events::room::message::RoomMessageEventContent,
+        OwnedUserId, RoomId, UserId,
+    },
+    Client, Session,
+};
+use serde::{Deserialize, Serialize};
+use sqlx::{pool::PoolConnection, postgres::PgQueryResult, Postgres};
+use url::Url;
+
+use crate::{
+    board::Board, entity::{Entity, Contact}, file::File, milestone::Milestone, project::Project, task::Task, note::Note,
+};
+
+// authentication, messaging and server management stuff for matrix.
+
+const INITIAL_DEVICE_DISPLAY_NAME: &str = "Kinbrio-client";
+
+pub struct Choice {
+    pub url: String,
+    pub display: String,
+    pub logo: String,
+}
+pub async fn get_login_urls(
+    homeserver_url: String,
+    redirect_url: String,
+) -> Result<Vec<Choice>, matrix_sdk::Error> {
+    let homeserver_url = Url::parse(&homeserver_url).expect("Url Correct");
+    let client = Client::new(homeserver_url)
+        .await
+        .expect("Matrix Server Connecting");
+    let mut choices = Vec::new();
+    let login_types = client
+        .get_login_types()
+        .await
+        .expect("Login types found")
+        .flows;
+    for login_type in login_types {
+        match login_type {
+            LoginType::Sso(sso) => {
+                if sso.identity_providers.is_empty() {
+                    choices.push(LoginChoice::Sso)
+                } else {
+                    choices.extend(sso.identity_providers.into_iter().map(LoginChoice::SsoIdp))
+                }
+            }
+            LoginType::Password(t) => {
+                choices.push(LoginChoice::Password)
+            }
+            LoginType::Token(_) | _ => {}
+        }
+    }
+    let mut urls = vec![];
+    for c in &choices {
+        let u = c
+            .login(&client, redirect_url.clone())
+            .await
+            .expect("login URL fails");
+        urls.push(Choice {
+            url: u.clone(),
+            display: c.to_string(),
+            logo: c.get_icon(),
+        });
+    }
+    return Ok(urls);
+}
+
+#[derive(Debug)]
+pub enum LoginChoice {
+    Password,
+    /// Login with SSO.
+    Sso,
+    /// Login with a specific SSO identity provider.
+    SsoIdp(IdentityProvider),
+}
+
+impl LoginChoice {
+    /// Login with this login choice.
+    async fn login(&self, client: &Client, redirect: String) -> anyhow::Result<String> {
+        match self {
+            LoginChoice::Password => login_with_password_url(client),
+            LoginChoice::Sso => login_with_sso_url(client, redirect, None).await,
+            LoginChoice::SsoIdp(idp) => login_with_sso_url(client, redirect, Some(idp)).await,
+        }
+    }
+
+    fn get_icon_mxc(&self) -> String {
+        match self {
+            LoginChoice::Password =>  "/fs/images/sso/user_password.svg".to_string(),
+            LoginChoice::Sso => "hmm".to_string(),
+            LoginChoice::SsoIdp(idp) => idp.icon.as_ref().expect("get icon URL").to_string(),
+        }
+    }
+
+    fn get_icon(&self) -> String {
+        match self {
+            LoginChoice::Password =>  "/fs/images/sso/user_password.svg".to_string(),
+            LoginChoice::Sso => "hmm".to_string(),
+            LoginChoice::SsoIdp(idp) => {
+                let mxc_uri = idp.icon.as_ref().expect("get icon URL").to_string();
+                mxc_uri.replace("mxc://", "https://matrix.org/_matrix/media/r0/download/")
+            },
+        }
+    }
+}
+
+impl fmt::Display for LoginChoice {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            LoginChoice::Password => write!(f, "Username and password"),
+            LoginChoice::Sso => write!(f, "SSO"),
+            LoginChoice::SsoIdp(idp) => write!(f, "{}", idp.name),
+        }
+    }
+}
+
+/// Login with a username and password.
+fn login_with_password_url(client: &Client) -> anyhow::Result<String> {
+    return Ok("/login_by_username".to_string())
+}
+
+/// Login with SSO.
+pub(crate) async fn restore_from_session(
+    homeserver_url: String,
+    session: Session,
+) -> anyhow::Result<()> {
+    let homeserver_url = Url::parse(&homeserver_url).expect("URL parsing");
+    let client = Client::new(homeserver_url)
+        .await
+        .expect("Matrix server connection");
+    client.restore_login(session).await.expect("Restore login");
+    Ok(())
+}
+
+pub(crate) async fn login_with_password(
+    homeserver_url: String,
+    uid: String,
+    password: String,
+) -> anyhow::Result<login::v3::Response> {
+    let homeserver_url = Url::parse(&homeserver_url).expect("URL parsing");
+    let client = Client::new(homeserver_url)
+        .await
+        .expect("Matrix server connection");
+    let login_builder = client.login_username(uid.as_str(), password.as_str()).send().await.expect("logged in");
+           
+    Ok(login_builder)
+}
+
+
+pub(crate) async fn account(
+    homeserver_url: String,
+    token: String,
+    user_id:OwnedUserId,
+) -> (std::string::String, std::string::String, std::string::String) {
+    let homeserver_url = Url::parse(&homeserver_url).expect("URL parsing");
+    let client = Client::new(homeserver_url)
+        .await
+        .expect("Matrix server connection"); 
+    client.restore_login(Session {
+        access_token: token,
+        refresh_token: None,
+        user_id: OwnedUserId::from(user_id),
+        device_id: device_id!("kinbrio").to_owned(),
+    }).await.expect("Restore");
+    let avatar = client.account().get_avatar_url().await.expect("Get avatar URL").expect("Unroll").to_string();
+    let threepids = client.account().get_3pids().await.expect("Get 3pid").threepids;
+    let mut addy = String::default();
+    for pid in threepids {
+        addy = match pid.medium {
+            matrix_sdk::ruma::thirdparty::Medium::Email => pid.address,
+            matrix_sdk::ruma::thirdparty::Medium::Msisdn => pid.address,
+            _ => String::default(),
+        };
+        if addy.len() > 0 {
+            break;
+        }
+    };
+    let display_name = client.account().get_profile().await.expect("get prorfile").displayname.unwrap_or_default();
+    (addy, avatar, display_name)
+}
+
+pub(crate) async fn login_with_token(
+    homeserver_url: String,
+    token: String,
+) -> anyhow::Result<login::v3::Response> {
+    let homeserver_url = Url::parse(&homeserver_url).expect("URL parsing");
+    let client = Client::new(homeserver_url)
+        .await
+        .expect("Matrix server connection");
+    
+    let login_builder = client.login_token(&token).send().await?;
+    Ok(login_builder)
+}
+
+async fn login_with_sso_url(
+    client: &Client,
+    redirect: String,
+    idp: Option<&IdentityProvider>,
+) -> anyhow::Result<String> {
+    
+    let login_builder = client
+        .get_sso_login_url(&redirect, Some(&idp.expect("get provider").id))
+        .await
+        .expect("Get provider");
+
+    Ok(login_builder)
+}
+
+pub async fn delete_room(
+    conn: &mut PoolConnection<Postgres>,
+    key: uuid::Uuid,
+) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM rooms where key=$1", key)
+        .execute(conn)
+        .await;
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Room {
+    pub key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+
+    pub name: String,
+    pub description: String,
+    pub matrix_room_url: String,
+    pub matrix_room_id: String,
+    pub message_types: MessageDataType,
+    pub alert_level: i16,
+
+    pub created: i64,
+    pub updated: i64,
+}
+
+pub async fn get_rooms(
+    conn: &mut PoolConnection<Postgres>,
+    organization_key: uuid::Uuid,
+) -> Vec<Room> {
+    let room_records = sqlx::query!(
+        "select 
+        key, 
+        owner_key, 
+        organization_key, 
+        name, 
+        description, 
+        matrix_room_url, 
+        matrix_room_id, 
+        message_types,
+        alert_level, 
+        created, 
+        updated from rooms 
+        where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select room by key");
+    let mut rooms = vec![];
+    for room in room_records {
+        let m_type: MessageDataType = room.message_types.expect("message_types exists").into();
+        rooms.push(Room {
+            key: room.key.expect("key exists"),
+            owner_key: room.owner_key.expect("owner_key exists"),
+            organization_key: room.organization_key.expect("organization_key exists"),
+            name: room.name.expect("name exists"),
+            description: room.description.expect("description exists"),
+            matrix_room_url: room.matrix_room_url.expect("matrix_room_url exists"),
+            matrix_room_id: room.matrix_room_id.expect("matrix_room_id exists"),
+            message_types: m_type,
+            alert_level: room.alert_level.expect("alert_level exists"),
+            created: room.created.expect("created exists"),
+            updated: room.updated.expect("updated exists"),
+        });
+    }
+    rooms
+}
+
+pub async fn get_room(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Room {
+    let room = sqlx::query!(
+        "select 
+        key, 
+        owner_key, 
+        organization_key, 
+        name, 
+        description, 
+        matrix_room_url, 
+        matrix_room_id, 
+        message_types,
+        alert_level, 
+        created, 
+        updated from rooms 
+        where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select room by key");
+    Room {
+        key: room.key.expect("key exists"),
+        owner_key: room.owner_key.expect("owner_key exists"),
+        organization_key: room.organization_key.expect("organization_key exists"),
+        name: room.name.expect("name exists"),
+        description: room.description.expect("description exists"),
+        matrix_room_url: room.matrix_room_url.expect("matrix_room_url exists"),
+        matrix_room_id: room.matrix_room_id.expect("matrix_room_id exists"),
+        message_types: MessageDataType::All,
+        alert_level: room.alert_level.expect("alert_level exists"),
+        created: room.created.expect("created exists"),
+        updated: room.updated.expect("updated exists"),
+    }
+}
+
+pub async fn insert_room(conn: &mut PoolConnection<Postgres>, new_room: &Room) {
+    let t = new_room.message_types as i16;
+    sqlx::query!(
+        "INSERT INTO rooms (
+        key, 
+        owner_key, 
+        organization_key, 
+        name, 
+        description, 
+        matrix_room_url, 
+        matrix_room_id, 
+        message_types,
+        alert_level, 
+        created, 
+        updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
+        new_room.key,
+        new_room.owner_key,
+        new_room.organization_key,
+        &new_room.name,
+        &new_room.description,
+        &new_room.matrix_room_url,
+        &new_room.matrix_room_id,
+        &t,
+        &new_room.alert_level,
+        new_room.created,
+        new_room.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type)]
+pub enum MessageDataType {
+    All = 0,
+    Board = 1,
+    Entity = 2,
+    File = 3,
+    Milestone = 4,
+    Oragnization = 5,
+    Project = 6,
+    Task = 7,
+    User = 8,
+    Report = 9,
+    Room = 10,
+}
+impl Into<MessageDataType> for i16 {
+    fn into(self) -> MessageDataType {
+        match self {
+            0 => MessageDataType::All,
+            1 => MessageDataType::Board,
+            2 => MessageDataType::Entity,
+            3 => MessageDataType::File,
+            4 => MessageDataType::Milestone,
+            5 => MessageDataType::Oragnization,
+            6 => MessageDataType::Project,
+            7 => MessageDataType::Task,
+            8 => MessageDataType::User,
+            9 => MessageDataType::Report,
+            10 => MessageDataType::Room,
+            _ => MessageDataType::All
+        }
+    }
+}
+impl From<MessageDataType> for i16 {
+    fn from(t: MessageDataType) -> Self {
+        match t {
+            MessageDataType::All => 0,
+            MessageDataType::Board => 1,
+            MessageDataType::Entity => 2,
+            MessageDataType::File => 3,
+            MessageDataType::Milestone => 4,
+            MessageDataType::Oragnization => 5,
+            MessageDataType::Project => 6,
+            MessageDataType::Task => 7,
+            MessageDataType::User => 8,
+            MessageDataType::Report => 9,
+            MessageDataType::Room => 10
+        }
+    }
+}
+
+enum MessageActionType {
+    All,
+    Create,
+    Update,
+    Delete,
+    Complete,
+}
+
+async fn message(
+    conn: &mut PoolConnection<Postgres>,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    homeserver_url: String,
+    token: String,
+    data_type: MessageDataType,
+    _action_type: MessageActionType,
+    msg: String,
+) -> Result<(), anyhow::Error> {
+    let rooms = get_rooms(conn, organization_id).await;
+    for room in rooms {
+        if room.message_types == data_type {
+            send_room_message(
+                matrix_user_id.clone(),
+                organization_id,
+                homeserver_url.clone(),
+                token.clone(),
+                room.matrix_room_id.clone(),
+                msg.clone(),
+            ).await.expect("message sent");
+        }
+    }
+    Ok(())
+}
+async fn send_room_message(
+    matrix_user_id: String,
+    _organization_id: uuid::Uuid,
+    homeserver_url: String,
+    token: String,
+    room_id: String,
+    msg: String,
+) -> Result<(), anyhow::Error> {
+    let user_id_str = matrix_user_id.to_string();
+    let user_id = <&UserId>::try_from(user_id_str.as_str()).expect("parse user id");
+    let homeserver_url =
+        Url::parse(&homeserver_url).unwrap_or(Url::parse("https://matrix-client.matrix.org")?);
+    let client = Client::new(homeserver_url).await?;
+    
+    let session = Session {
+        access_token: token,
+        refresh_token: None,
+        user_id: OwnedUserId::from(user_id),
+        device_id: device_id!("kinbrio").to_owned(),
+    };
+    client.restore_login(session).await.expect("Send login");
+    client
+        .sync_once(SyncSettings::default())
+        .await
+        .expect("sync");
+    let room_id = <&RoomId>::try_from(room_id.as_str()).expect("parse room id");
+    let room = client.get_joined_room(room_id).expect("Retrieve room");
+
+    let content = RoomMessageEventContent::text_plain(msg);
+    room.send(content, None).await.expect("Send room");
+    Ok(())
+}
+
+pub(crate) async fn post_room_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    room: &Room,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Room 🚀 \n {} \n `{}`\n  https://kinbrio.com/room/{}",
+        room.name, room.description, room.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Room,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_file_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    file: &File,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New File 🚀 \n {}\n `{}`\n  https://kinbrio.com/file/{}",
+        file.name, file.description, file.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::File,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_milestone_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    milestone: &Milestone,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Milestone 🚀 \n {}\n `{}`\n  https://kinbrio.com/milestone/{}",
+        milestone.name, milestone.description, milestone.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Milestone,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_project_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    project: &Project,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Project 🚀 \n {}\n `{}`\n  https://kinbrio.com/project/{}",
+        project.name, project.description, project.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Project,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_entity_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    entity: &Entity,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Entity Added 🚀 \n  {}\n `{}`\n  https://kinbrio.com/entity/{}",
+        entity.name, entity.description, entity.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Entity,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_note_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    note: &Note,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Note Added 🚀 \n  {} \n  https://kinbrio.com/contact/{}",
+        note.title, note.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Entity,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+pub(crate) async fn post_contact_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    contact: &Contact,
+) -> Result<(), anyhow::Error> {
+    let msg: String = format!(
+        "New Contact Added 🚀 \n  {} {}\n  https://kinbrio.com/contact/{}",
+        contact.first_name, contact.last_name, contact.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Entity,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_board_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    board: &Board,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Board 🚀 \n  {}\n `{}`\n  https://kinbrio.com/board/{}",
+        board.name, board.description, board.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Board,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}
+
+pub(crate) async fn post_task_create(
+    conn: &mut PoolConnection<Postgres>,
+    homeserver_url: String,
+    matrix_user_id: String,
+    organization_id: uuid::Uuid,
+    token: String,
+    
+    task: &Task,
+) -> Result<(), anyhow::Error> {
+    let msg = format!(
+        "New Task 🚀 \n {} day(s) Task: {}\n `{}`\n  https://kinbrio.com/task/{}",
+        task.estimated_quarter_days as f64 * 0.25,
+        task.name,
+        task.description,
+        task.key
+    );
+    message(
+        conn,
+        matrix_user_id,
+        organization_id,
+        homeserver_url,
+        token,
+        MessageDataType::Task,
+        MessageActionType::Create,
+        msg,
+    )
+    .await?;
+    Ok(())
+}

+ 333 - 0
src/milestone.rs

@@ -0,0 +1,333 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::matrix::post_milestone_create;
+use crate::{State, user};
+ 
+// SQL STUFF
+
+pub async fn get_milestones_by_project(conn: &mut PoolConnection<Postgres>, project_key: uuid::Uuid) -> Vec::<Milestone> {
+    let milestone_records = sqlx::query!(
+        "select key, organization_key, owner_key, project_key, name, description, tags, estimated_quarter_days, start, due, created, updated from mile_stones where project_key = $1",
+        project_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select milestone by key");
+    let mut milestones = vec![];
+    for milestone in milestone_records { 
+        milestones.push(Milestone {
+            key: milestone.key.expect("key exists"),
+            organization_key: milestone.organization_key.expect("organization_key"), 
+            owner_key: milestone.owner_key.expect("owner_key exists"),
+            project_key: milestone.project_key.expect("project_key exists"),
+            name: milestone.name.expect("name exists"),
+            description: milestone.description.expect("description exists"),
+            tags: milestone.tags.expect("tags exists"),
+            estimated_quarter_days: milestone.estimated_quarter_days.expect("estimated_quarter_days exists"),
+            start: milestone.start.expect("start"),
+            due:milestone.due.expect("due"),
+            created: milestone.created.expect("created exists"),
+            updated: milestone.updated.expect("updated exists"),
+        })
+    }
+    milestones
+}
+
+pub async fn get_milestone(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Milestone {
+    let milestone = sqlx::query!(
+        "select key, organization_key, owner_key, project_key, name, description, tags, estimated_quarter_days, start, due, created, updated from mile_stones where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select milestone by key");
+
+    Milestone {
+        key: milestone.key.expect("key exists"),
+        organization_key: milestone.organization_key.expect("organization_key"), 
+        owner_key: milestone.owner_key.expect("owner_key exists"),
+        project_key: milestone.project_key.expect("project_key exists"),
+        name: milestone.name.expect("name exists"),
+        description: milestone.description.expect("description exists"),
+        tags: milestone.tags.expect("tags exists"),
+        estimated_quarter_days: milestone.estimated_quarter_days.expect("estimated_quarter_days exists"),
+        start: milestone.start.expect("start"),
+        due:milestone.due.expect("due"),
+        created: milestone.created.expect("created exists"),
+        updated: milestone.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_milestone(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM mile_stones where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+
+async fn update_milestone(conn: &mut PoolConnection<Postgres>, board: &Milestone) {
+    sqlx::query!("UPDATE mile_stones SET name=$1, description=$2, tags=$3, 
+    estimated_quarter_days=$4, start=$5, due=$6 where key=$7",
+    &board.name,
+    &board.description,
+    &board.tags, 
+    &board.estimated_quarter_days, 
+    &board.start, 
+    &board.due, 
+    board.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+async fn insert_milestone(conn: &mut PoolConnection<Postgres>, new_milestone: &Milestone) {
+    sqlx::query!("INSERT INTO mile_stones (key, organization_key, project_key, owner_key, name, description, tags, estimated_quarter_days, start, due, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", 
+        new_milestone.key,
+        new_milestone.organization_key, 
+        new_milestone.project_key, 
+        new_milestone.owner_key,
+        &new_milestone.name,
+        &new_milestone.description,
+        &new_milestone.tags,
+        new_milestone.estimated_quarter_days,
+        new_milestone.start,
+        new_milestone.due,
+        new_milestone.created,
+        new_milestone.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("milestone_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Milestone uuid parse");
+            match delete_milestone(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    match req.param("milestone_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Milestone uuid parse");
+            let milestone = get_milestone(&mut conn, s_uuid).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    MilestoneTemplate::new(
+                        milestone,
+                        u,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Milestone, tide::Error> = req.body_json().await;
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    }; 
+    match umd {
+        Ok(milestone) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            
+            if milestone.key == uuid::Uuid::nil() {
+                let s = Milestone::new(milestone.organization_key, milestone.owner_key, milestone.project_key, milestone.name, milestone.description, milestone.tags, milestone.estimated_quarter_days, milestone.start, milestone.due);
+                insert_milestone(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_milestone_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            } else {
+                update_milestone(&mut conn, &milestone).await;
+                let j = serde_json::to_string(&milestone).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+            
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    
+    match req.param("project_id") {
+        Ok(project_id) => { 
+            let project_id = uuid::Uuid::from_str(project_id).expect("Project uuid parse");
+            let mut milestone= Milestone::new( u.organization_key, u.key, project_id,  "".to_owned(), "".to_owned(), "".to_owned(),  0, 0, 0);
+            milestone.key = uuid::Uuid::nil();
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+            .content_type(mime::HTML)
+            .body(
+                MilestoneTemplate::new(
+                    milestone,
+                    u,
+                )
+                .render_string(),
+            )
+            .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+// data types
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Milestone {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub project_key: uuid::Uuid,
+    pub name: String,
+    pub description: String,
+    pub tags: String,
+
+    pub estimated_quarter_days: i32,
+    pub start: i64,
+    pub due: i64,
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl Milestone {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        project_key: uuid::Uuid,
+        name: String,
+        description: String,
+        tags: String,
+        estimated_quarter_days:i32,
+        start: i64,
+        due: i64,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            project_key,
+            owner_key,
+            name,
+            description,
+            tags,
+            estimated_quarter_days,
+            start,
+            due,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "milestone.html")]
+pub struct MilestoneTemplate {
+    milestone: Milestone,
+    user: crate::user::User,
+}
+
+impl<'a> MilestoneTemplate {
+    pub fn new(
+        milestone: Milestone,
+        user: crate::user::User,
+    ) -> Self {
+        return Self {
+            milestone,
+            user,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 385 - 0
src/note.rs

@@ -0,0 +1,385 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::file::AssociationType;
+use crate::home::NotFoundTemplate;
+use crate::matrix::post_note_create;
+use crate::{State, user};
+ 
+// SQL STUFF
+pub async fn get_associated_notes(conn: &mut PoolConnection<Postgres>, association_type: AssociationType, associated_key: uuid::Uuid) -> Vec<Note> {
+    let records = sqlx::query!(
+        "select key, organization_key, owner_key, association_type, association_key, title, content, url, created, updated from notes 
+        where association_type= $1 AND association_key = $2",
+        association_type as i16, associated_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut notes = Vec::<Note>::new();
+    for note in records {
+        let association_type: AssociationType = note.association_type.expect("association_type exists").into();
+        let n= Note {
+            key: note.key.expect("key exists"),
+            organization_key: note.organization_key.expect("organization_key"), 
+            owner_key: note.owner_key.expect("owner_key exists"),
+            association_type, 
+            association_key: note.association_key.expect("association_key exists"), 
+            title: note.title.expect("title exists"),
+            content: note.content.expect("content exists"),
+            url: note.url.expect("url exists"),
+            created: note.created.expect("created exists"),
+            updated: note.updated.expect("updated exists"),
+        };
+        notes.push(n);
+    }
+    return notes
+}
+
+pub async fn get_organization_notes(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec<Note> {
+    let records = sqlx::query!(
+        "select key, organization_key, owner_key, association_type, association_key, title, content, url, created, updated from notes where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut notes = Vec::<Note>::new();
+    for note in records {
+        let association_type: AssociationType = note.association_type.expect("association_type exists").into();
+        let n = Note {
+            key: note.key.expect("key exists"),
+            organization_key: note.organization_key.expect("organization_key"), 
+            owner_key: note.owner_key.expect("owner_key exists"),
+            association_type, 
+            association_key: note.association_key.expect("association_key exists"), 
+            title: note.title.expect("title exists"),
+            content: note.content.expect("content exists"),
+            url: note.url.expect("url exists"),
+            created: note.created.expect("created exists"),
+            updated: note.updated.expect("updated exists"),
+        };
+        notes.push(n);
+    }
+    return notes
+}
+
+pub async fn get_user_notes(conn: &mut PoolConnection<Postgres>, user_key: uuid::Uuid) -> Vec<Note> {
+    let records = sqlx::query!(
+        "select key, organization_key, owner_key, association_type, association_key, title, content, url, created, updated from notes where owner_key = $1",
+        user_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut notes = Vec::<Note>::new();
+    for note in records {
+        let association_type: AssociationType = note.association_type.expect("association_type exists").into();
+        let n: Note = Note {
+            key: note.key.expect("key exists"),
+            organization_key: note.organization_key.expect("organization_key"), 
+            owner_key: note.owner_key.expect("owner_key exists"),
+            association_type, 
+            association_key: note.association_key.expect("association_key exists"), 
+            title: note.title.expect("title exists"),
+            content: note.content.expect("content exists"),
+            url: note.url.expect("url exists"),
+            created: note.created.expect("created exists"),
+            updated: note.updated.expect("updated exists"),
+        };
+        notes.push(n);
+    }
+    return notes
+}
+
+pub async fn get_note(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Note {
+    let note = sqlx::query!(
+        "select key, organization_key, owner_key, association_type, association_key, title, content, url, created, updated from notes where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select note by key");
+    let association_type: AssociationType = note.association_type.expect("association_type exists").into();
+    Note {
+        key: note.key.expect("key exists"),
+        organization_key: note.organization_key.expect("organization_key"), 
+        owner_key: note.owner_key.expect("owner_key exists"),
+        association_type, 
+        association_key: note.association_key.expect("association_key exists"), 
+        title: note.title.expect("title exists"),
+        content: note.content.expect("content exists"),
+        url: note.url.expect("url exists"),
+        created: note.created.expect("created exists"),
+        updated: note.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_note(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM notes where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+async fn update_note(conn: &mut PoolConnection<Postgres>, note: &Note) {
+    sqlx::query!("UPDATE notes SET title=$1, content=$2, association_type=$3, 
+    association_key=$4, url=$5 where key=$6",  
+    &note.title,
+    &note.content,
+    note.association_type as i16, 
+    &note.association_key, 
+    &note.url, 
+    note.key,
+)
+.execute(conn)
+.await
+.expect("Insert Success");
+}
+
+async fn insert_note(conn: &mut PoolConnection<Postgres>, new_note: &Note) {
+    sqlx::query!("INSERT INTO notes (key, organization_key, owner_key, association_type, association_key, title, content, url, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", 
+        new_note.key,
+        new_note.organization_key, 
+        new_note.owner_key,
+        new_note.association_type as i16,
+        new_note.association_key,
+        &new_note.title,
+        &new_note.content,
+        &new_note.url, 
+        new_note.created,
+        new_note.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+
+    let ass_id = match req.param("association_id") {
+        Ok(v) => v,
+        Err(_) => {
+            return Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+            .content_type(mime::HTML)
+            .body(NotFoundTemplate::new().render_string())
+            .build())
+        },
+    };
+
+    let ass_type = match req.param("association_type") {
+        Ok(v) => v,
+        Err(_) => {
+            return Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+            .content_type(mime::HTML)
+            .body(NotFoundTemplate::new().render_string())
+            .build())
+        },
+    };
+    let associated_uuid = uuid::Uuid::parse_str(ass_id).expect("associated uuid valid");
+    let association_type: AssociationType = AssociationType::from_str(ass_type).expect("Parse type");
+    let mut note= Note::new( u.organization_key, u.key, association_type, associated_uuid, "".to_owned(), "".to_owned(), "".to_owned());
+    note.key = uuid::Uuid::nil();
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+    .content_type(mime::HTML)
+    .body(
+        NoteTemplate::new(
+            note,
+            u,
+        )
+        .render_string(),
+    )
+    .build())
+}
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("note_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Note uuid parse");
+            match delete_note(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("note_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Note uuid parse");
+            let note = get_note(&mut conn, s_uuid).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    NoteTemplate::new(
+                        note,
+                        u,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Note, tide::Error> = req.body_json().await;
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    match umd {
+        Ok(note) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            if note.key == uuid::Uuid::nil() {
+                let s = Note::new(note.organization_key, note.owner_key, note.association_type, note.association_key, note.url, note.title, note.content);
+                insert_note(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_note_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            } else {
+                update_note(&mut conn, &note).await;
+                let j = serde_json::to_string(&note).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+// data types
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Note {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid, 
+    pub owner_key: uuid::Uuid,
+    pub association_type: AssociationType,
+    pub association_key: uuid::Uuid,
+    pub title: String,
+    pub content: String,
+    pub url: String,
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl Note {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        association_type: AssociationType,
+        association_key: uuid::Uuid,
+        url: String,
+        title: String,
+        content: String,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            association_type,
+            association_key,
+            url,
+            owner_key,
+            title,
+            content,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "note.html")]
+pub struct NoteTemplate {
+    note: Note,
+    user: crate::user::User,
+}
+
+impl<'a> NoteTemplate {
+    pub fn new(
+        note: Note,
+        user: crate::user::User,
+    ) -> Self {
+        return Self {
+            note,
+            user,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 299 - 0
src/organization.rs

@@ -0,0 +1,299 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::postgres::PgQueryResult;
+use sqlx::Postgres;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::{user, State};
+
+// SQL STUFF
+
+pub async fn get_organization(
+    conn: &mut PoolConnection<Postgres>,
+    key: uuid::Uuid,
+) -> Organization {
+    let organization = sqlx::query!(  
+        "select key, external_accounting_id, external_accounting_url, owner_key, name, description, matrix_home_server, matrix_live_support_room_url, matrix_general_room_url, domain, contact_email, created, updated from organization where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select organization by key");
+
+    Organization {
+        key: organization.key.expect("key exists"),
+        external_accounting_id: organization.external_accounting_id.expect("external_accounting_id exists"),
+        external_accounting_url: organization.external_accounting_url.expect("external_accounting_url exists"),
+        owner_key: organization.owner_key.expect("owner_key exists"),
+        name: organization.name.expect("name exists"),
+        description: organization.description.expect("description exists"),
+        matrix_home_server: organization.matrix_home_server.expect("matrix_home_server exists"),
+        matrix_live_support_room_url: organization.matrix_live_support_room_url.expect("matrix_live_support_room_url exists"),
+        matrix_general_room_url: organization.matrix_general_room_url.expect("matrix_general_room_url exists"),
+        domain: organization.domain.expect("domain exists"),
+        contact_email: organization.contact_email.expect("contact_email exists"),
+        created: organization.created.expect("created exists"),
+        updated: organization.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_organization(
+    conn: &mut PoolConnection<Postgres>,
+    key: uuid::Uuid,
+    owner_key: uuid::Uuid,
+) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!(
+        "DELETE FROM organization where owner_key=$1 AND key=$2",
+        owner_key,
+        key
+    )
+    .execute(conn)
+    .await;
+}
+
+pub(crate) async fn insert_organization(
+    conn: &mut PoolConnection<Postgres>,
+    new_organization: &Organization,
+) {
+    sqlx::query!("INSERT INTO organization (key, external_accounting_id, external_accounting_url, owner_key, name, description, matrix_home_server, matrix_live_support_room_url, matrix_general_room_url, domain, contact_email, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", 
+        new_organization.key,
+        new_organization.external_accounting_id,
+        new_organization.external_accounting_url,
+        new_organization.owner_key,
+        &new_organization.name,
+        &new_organization.description,
+        new_organization.matrix_home_server,
+        new_organization.matrix_live_support_room_url,
+        new_organization.matrix_general_room_url,
+        new_organization.domain,
+        new_organization.contact_email,
+        new_organization.created,
+        new_organization.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("organization_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Organization uuid parse");
+            match delete_organization(&mut conn, s_uuid, u.key).await {
+                Ok(_) => Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::HTML)
+                    .build()),
+                Err(_) => Ok(
+                    tide::Response::builder(tide::StatusCode::InternalServerError)
+                        .content_type(mime::HTML)
+                        .body(NotFoundTemplate::new().render_string())
+                        .build(),
+                ),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    match req.param("organization_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("Organization uuid parse");
+            let organization = get_organization(&mut conn, s_uuid).await;
+            
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(OrganizationTemplate::new(organization, u).render_string())
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn update_organization(conn: &mut PoolConnection<Postgres>, organization: &Organization) {
+    sqlx::query!(
+        "UPDATE organization SET name=$1, description=$2, external_accounting_id=$3, external_accounting_url=$4, matrix_home_server=$5, matrix_live_support_room_url=$6, matrix_general_room_url=$7, domain=$8, contact_email=$9 where key=$10",
+        &organization.name,
+        &organization.description,
+        organization.external_accounting_id,
+        organization.external_accounting_url,
+        organization.matrix_home_server,
+        organization.matrix_live_support_room_url,
+        organization.matrix_general_room_url,
+        organization.domain,
+        organization.contact_email,
+        organization.key,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Organization, tide::Error> = req.body_json().await;
+
+    let _claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match umd {
+        Ok(organization) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            if organization.key == uuid::Uuid::nil() {
+                let s = Organization::new(
+                    Uuid::new_v4(),
+                    organization.external_accounting_id,
+                    organization.external_accounting_url,
+                    organization.owner_key,
+                    organization.name,
+                    organization.description,
+                    organization.matrix_home_server,
+                    organization.matrix_live_support_room_url,
+                    organization.matrix_general_room_url,
+                    organization.domain,
+                    organization.contact_email,
+                );
+                insert_organization(&mut conn, &s).await;
+                let j = serde_json::to_string(&s).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build());
+            }
+
+            update_organization(&mut conn, &organization).await;
+            let j = serde_json::to_string(&organization).expect("To JSON");
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+// data types
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Organization {
+    pub key: uuid::Uuid,
+    pub external_accounting_id: String,
+    pub external_accounting_url: String,
+    pub owner_key: uuid::Uuid,
+    pub name: String,
+    pub description: String,
+    pub matrix_home_server: String,
+    pub matrix_live_support_room_url: String,
+    pub matrix_general_room_url: String,
+    pub domain: String,
+    pub contact_email: String,
+    
+    pub created: i64,
+    pub updated: i64,
+}
+
+impl Organization {
+    pub fn new(key: uuid::Uuid, 
+        external_accounting_id: String,
+        external_accounting_url: String,
+        owner_key: uuid::Uuid, 
+        name: String, 
+        description: String,
+        matrix_home_server: String,
+        matrix_live_support_room_url: String,
+        matrix_general_room_url: String,
+        domain: String,
+        contact_email: String,
+    ) -> Self {
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            external_accounting_id,
+            external_accounting_url,
+            owner_key,
+            name,
+            description,
+            matrix_home_server,
+            matrix_live_support_room_url,
+            matrix_general_room_url,
+            domain,
+            contact_email,
+            created,
+            updated,
+        }
+    }
+
+    pub fn has_external_accounting(&self) -> bool {
+        return self.external_accounting_id.len() > 0;
+    }
+}
+
+#[derive(Template)]
+#[template(path = "organization.html")]
+pub struct OrganizationTemplate {
+    organization: Organization,
+    user: crate::user::User,
+}
+
+impl<'a> OrganizationTemplate {
+    pub fn new(organization: Organization, user: crate::user::User) -> Self {
+        return Self { organization, user };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 379 - 0
src/project.rs

@@ -0,0 +1,379 @@
+use std::str::FromStr;
+
+use askama::Template;
+use chrono::Datelike;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use uuid::Uuid;
+use strum::IntoEnumIterator;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::milestone::{Milestone, get_milestones_by_project};
+use crate::{State, user, entity, note, file};
+use crate::matrix::post_project_create;
+use crate::task::{Task, get_tasks_by_project, TaskStatus};
+ 
+// SQL STUFF
+pub async fn get_projects_by_organization(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec<Project> {
+    let records = sqlx::query!(
+        "select key, owner_key, organization_key, name, description, tags, estimated_quarter_days, start, due, created, updated from projects where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut projects = Vec::<Project>::new();
+    for project in records {
+        let prj = Project {
+            key: project.key.expect("key exists"),
+            organization_key: project.organization_key.expect("organization_key"), 
+            owner_key: project.owner_key.expect("owner_key exists"),
+            name: project.name.expect("name exists"),
+            description: project.description.expect("description exists"),
+            tags: project.tags.expect("tags exists"),
+            estimated_quarter_days: project.estimated_quarter_days.expect("estimated_quarter_days exists"),
+            start: project.start.expect("start"),
+            due:project.due.expect("due"),
+            created: project.created.expect("created exists"),
+            updated: project.updated.expect("updated exists"),
+        };
+        projects.push(prj);
+    }
+    return projects
+}
+
+pub async fn get_project(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Project {
+    let project = sqlx::query!(
+        "select key, owner_key, organization_key, name, description, tags, estimated_quarter_days, start, due, created, updated from projects where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select project by key");
+
+    Project {
+        key: project.key.expect("key exists"),
+        organization_key: project.organization_key.expect("organization_key"), 
+        owner_key: project.owner_key.expect("owner_key exists"),
+        name: project.name.expect("name exists"),
+        description: project.description.expect("description exists"),
+        tags: project.tags.expect("tags exists"),
+        estimated_quarter_days: project.estimated_quarter_days.expect("estimated_quarter_days exists"),
+        start: project.start.expect("start"),
+        due:project.due.expect("due"),
+        created: project.created.expect("created exists"),
+        updated: project.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_project(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM projects where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+ 
+async fn update_project(conn: &mut PoolConnection<Postgres>, project: &Project) {
+    sqlx::query!("UPDATE projects SET name=$1, description=$2, tags=$3, estimated_quarter_days=$4, start=$5, due=$6 where key=$7", 
+        &project.name,
+        &project.description,
+        &project.tags,
+        project.estimated_quarter_days,
+        project.start,
+        project.due,
+        project.key,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+async fn insert_project(conn: &mut PoolConnection<Postgres>, new_project: &Project) {
+    sqlx::query!("INSERT INTO projects (key, organization_key, owner_key, name, description, tags, estimated_quarter_days, start, due, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", 
+        new_project.key,
+        new_project.organization_key, 
+        new_project.owner_key,
+        &new_project.name,
+        &new_project.description,
+        &new_project.tags,
+        new_project.estimated_quarter_days,
+        new_project.start,
+        new_project.due,
+        new_project.created,
+        new_project.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("project_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Project uuid parse");
+            match delete_project(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    let mut project= Project::new( uuid::Uuid::nil(),  uuid::Uuid::nil(),  "".to_owned(), "".to_owned(), "".to_owned(),  0, 0, 0);
+    project.key = uuid::Uuid::nil();
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+    .content_type(mime::HTML)
+    .body(
+        ProjectTemplate::new(
+            project,
+            vec![],
+            vec![],
+            u,
+            vec![],
+            vec![],
+            vec![],
+            vec![],
+        )
+        .render_string(),
+    )
+    .build())
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    match req.param("project_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await.expect("Acquiring connection");
+            let p_uuid = uuid::Uuid::from_str(key).expect("Project uuid parse");
+            let project = get_project(&mut conn, p_uuid).await;
+            let tasks = get_tasks_by_project(&mut conn, project.key).await;
+            let milestones = get_milestones_by_project(&mut conn, project.key).await;
+
+            let entitys = entity::get_organization_entitys(&mut conn, u.organization_key).await;
+            let contacts = entity::get_contacts(&mut conn, u.organization_key).await;
+            let notes = note::get_associated_notes(&mut conn, crate::file::AssociationType::Project, p_uuid).await;
+            let files = file::get_associated_files(&mut conn, crate::file::AssociationType::Project, p_uuid).await;
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    ProjectTemplate::new(
+                        project,
+                        tasks,
+                        milestones,
+                        u,
+                        entitys,
+                        contacts,
+                        notes, 
+                        files,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Project, tide::Error> = req.body_json().await;
+
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    
+    match umd {
+        Ok(project) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            if project.key == uuid::Uuid::nil() {
+                let s = Project::new(project.organization_key, project.owner_key, project.name, project.description, project.tags, project.estimated_quarter_days, project.start, project.due);
+                insert_project(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_project_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+            
+                let j = serde_json::to_string(&s).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build())
+            }
+
+            update_project(&mut conn, &project).await;
+            let j = serde_json::to_string(&project).expect("To JSON");
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+// data types
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Project {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub name: String,
+    pub description: String,
+    pub tags: String,
+
+    pub estimated_quarter_days: i32,
+    pub start: i64,
+    pub due: i64,
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl Project {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        name: String,
+        description: String,
+        tags: String,
+        estimated_quarter_days:i32,
+        start: i64,
+        due: i64,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            owner_key,
+            name,
+            description,
+            tags,
+            estimated_quarter_days,
+            start,
+            due,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "project.html")]
+pub struct ProjectTemplate {
+    project: Project,
+    tasks: Vec<Task>,
+    milestones: Vec<Milestone>,
+    user: crate::user::User,
+    
+    entitys: Vec<entity::Entity>,
+    contacts: Vec<entity::Contact>,
+    notes: Vec<note::Note>,
+    files: Vec<file::File>,
+}
+
+impl<'a> ProjectTemplate {
+    pub fn new(
+        project: Project,
+        tasks: Vec<Task>,
+        milestones: Vec<Milestone>,
+        user: crate::user::User,
+        entitys: Vec<entity::Entity>,
+        contacts: Vec<entity::Contact>,
+        notes: Vec<note::Note>,
+        files: Vec<file::File>,
+    ) -> Self {
+        return Self {
+            project,
+            tasks,
+            milestones,
+            user,
+            entitys,
+            contacts,
+            notes, 
+            files,
+        };
+    }
+    
+    pub fn get_task_background_color<'aa>(&'aa self, status: &TaskStatus) -> String { 
+        match *status {
+            TaskStatus::Wishlist => "#cc66ff85".to_string(),
+            TaskStatus::Todo => "#ff6a4a85".to_string(),
+            TaskStatus::PlanningAndEstimating => "#ffb26085".to_string(),
+            TaskStatus::InQueue => "#f9ff5b85".to_string(),
+            TaskStatus::InProgress => "#61d1d085".to_string(),
+            TaskStatus::ToReview => "#658cff85".to_string(),
+            TaskStatus::InReviewal => "#D2CAFF85".to_string(),
+            TaskStatus::Complete => "#64ff6385".to_string(),
+        }
+    }
+
+
+    pub fn get_grid_column<'aa>(&'aa self, start: &i64, due: &i64) -> String {
+        let start = chrono::NaiveDateTime::from_timestamp_opt(*start,0).expect("Parsed start date");
+        let due = chrono::NaiveDateTime::from_timestamp_opt(*due,0).expect("Parsed due date");
+        let col = start.date().weekday().num_days_from_monday() + 1;
+        let difference = due.date()- start.date();
+        format!("{} / span {}", col, difference.num_days())
+    }
+    
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 425 - 0
src/service_item.rs

@@ -0,0 +1,425 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use strum_macros::EnumIter;
+use strum::IntoEnumIterator;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::organization::{get_organization, Organization};
+use crate::{State, user};
+ 
+// SQL STUFF
+
+pub async fn get_organization_service_items(conn: &mut PoolConnection<Postgres>, organization_key: uuid::Uuid) -> Vec<ServiceItem> {
+    let records = sqlx::query!(
+        "select  key, owner_key, organization_key, external_accounting_id, name, description, value, currency, service_item_type, service_value_type, expenses, created, updated from service_items where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select project by key");
+    let mut service_items = Vec::<ServiceItem>::new();
+    for service_item in records { 
+        let service_item_type: ServiceItemType = service_item.service_item_type.expect("service_item_type exists").into();
+        let service_value_type: ServiceValueType = service_item.service_value_type.expect("service_value_type exists").into();
+        let svc = ServiceItem {
+            key: service_item.key.expect("key exists"),
+            organization_key: service_item.organization_key.expect("organization_key"), 
+            external_accounting_id: service_item.external_accounting_id.expect("external_accounting_id"),
+            owner_key: service_item.owner_key.expect("owner_key exists"),
+            name: service_item.name.expect("name exists"),
+            description: service_item.description.expect("description exists"), 
+            value: service_item.value.expect("value exists"),
+            currency: service_item.currency.expect("currency exists"), 
+            service_item_type,
+            service_value_type,
+            expenses: service_item.expenses.expect("expenses exists"),
+            created: service_item.created.expect("created exists"),
+            updated: service_item.updated.expect("updated exists"),
+        };
+        service_items.push(svc);
+    }
+    return service_items
+}
+
+pub async fn get_service_item(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> ServiceItem {
+    let service_item = sqlx::query!(
+        "select key, owner_key, organization_key, external_accounting_id, name, description, value, currency, service_item_type, service_value_type, expenses, created, updated from service_items where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select service_item by key"); 
+    
+    let service_item_type: ServiceItemType = service_item.service_item_type.expect("service_item_type exists").into();
+    let service_value_type: ServiceValueType = service_item.service_value_type.expect("service_value_type exists").into();
+    ServiceItem {
+        key: service_item.key.expect("key exists"),
+        organization_key: service_item.organization_key.expect("organization_key"), 
+        external_accounting_id: service_item.external_accounting_id.expect("external_accounting_id"), 
+        owner_key: service_item.owner_key.expect("owner_key exists"),
+        name: service_item.name.expect("name exists"),
+        description: service_item.description.expect("description exists"),
+        value: service_item.value.expect("value exists"),
+        currency: service_item.currency.expect("currency exists"), 
+        service_item_type,
+        service_value_type,
+        expenses: service_item.expenses.expect("expenses exists"),
+        created: service_item.created.expect("created exists"),
+        updated: service_item.updated.expect("updated exists"),
+    }
+}
+
+async fn delete_service_item(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM service_items where owner_key=$1 AND key=$2", owner_key, key)
+    .execute(conn)
+    .await;
+}
+
+async fn update_service_item(conn: &mut PoolConnection<Postgres>, service_item: &ServiceItem) {
+    sqlx::query!("UPDATE service_items SET name=$1, description=$2, value=$3, currency=$4, service_item_type=$5, service_value_type=$6, expenses=$7, external_accounting_id=$8 where key=$9", 
+        &service_item.name,
+        &service_item.description,
+        &service_item.value,
+        service_item.currency,
+        service_item.service_item_type as i16,
+        service_item.service_value_type as i16,
+        &service_item.expenses,
+        &service_item.external_accounting_id,
+        service_item.key,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+pub async fn insert_service_item(conn: &mut PoolConnection<Postgres>, new_service_item: &ServiceItem) {
+    sqlx::query!("INSERT INTO service_items (key, organization_key, owner_key, name, description, value, currency, service_item_type, service_value_type, expenses, external_accounting_id, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", 
+        new_service_item.key,
+        new_service_item.organization_key, 
+        new_service_item.owner_key,
+        new_service_item.name,
+        new_service_item.description,
+        new_service_item.value, 
+        new_service_item.currency, 
+        new_service_item.service_item_type as i16, 
+        new_service_item.service_value_type as i16, 
+        &new_service_item.expenses, 
+        &new_service_item.external_accounting_id,
+        new_service_item.created,
+        new_service_item.updated,
+    )
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+ 
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    let mut service_item= ServiceItem::new( u.organization_key, String::default(), u.key,  "".to_owned(), "".to_owned(), 0, "USD".to_string(),  ServiceItemType::Service, ServiceValueType::Full, vec![]);
+    service_item.key = uuid::Uuid::nil();
+    let mut conn = req.state().db_pool.acquire().await?;
+    let organization = get_organization(&mut conn, u.organization_key).await;
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+    .content_type(mime::HTML)
+    .body(
+        ServiceItemTemplate::new(
+            service_item,
+            organization,
+            u,
+        )
+        .render_string(),
+    )
+    .build())
+}
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("service_item_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("ServiceItem uuid parse");
+            match delete_service_item(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::read_jwt_cookie_to_user(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+
+    match req.param("service_item_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let s_uuid = uuid::Uuid::from_str(key).expect("ServiceItem uuid parse");
+            let service_item = get_service_item(&mut conn, s_uuid).await;
+            let organization = get_organization(&mut conn, u.organization_key).await;
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    ServiceItemTemplate::new(
+                        service_item,
+                        organization,
+                        u,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<ServiceItem, tide::Error> = req.body_json().await;
+    let _claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+    match umd {
+        Ok(service_item) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            if service_item.key == uuid::Uuid::nil() { 
+                let s = ServiceItem::new(service_item.organization_key,service_item.external_accounting_id, service_item.owner_key, service_item.name, service_item.description,
+                     service_item.value, service_item.currency, service_item.service_item_type, service_item.service_value_type, service_item.expenses);
+                insert_service_item(&mut conn, &s).await;
+                //let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                // post_service_item_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build());
+            }
+            
+            update_service_item(&mut conn, &service_item).await;
+            let j = serde_json::to_string(&service_item).expect("To JSON");
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type, EnumIter)]
+pub enum ServiceValueType {
+    Hourly,
+    Milestone,
+    Full,
+}
+
+impl Into<ServiceValueType> for i16 {
+    fn into(self) -> ServiceValueType {
+        match self {
+            0 => ServiceValueType::Hourly,
+            1 => ServiceValueType::Milestone,
+            2 => ServiceValueType::Full,
+            _ => ServiceValueType::Hourly
+        }
+    }
+}
+
+impl FromStr for ServiceValueType {
+    type Err = ();
+    fn from_str(input: &str) -> Result<ServiceValueType, Self::Err> {
+        match input {
+            "Hourly Rate" => Ok(ServiceValueType::Hourly),
+            "Milestone Completion" => Ok(ServiceValueType::Milestone),
+            "Upon Completion" => Ok(ServiceValueType::Full),
+            "Full" => Ok(ServiceValueType::Full),
+            _ => Ok(ServiceValueType::Hourly),
+        }
+    }
+}
+
+impl ToString for ServiceValueType {
+    fn to_string(&self) -> String {
+        match self {
+            ServiceValueType::Hourly => "Hourly Rate".to_string(),
+            ServiceValueType::Milestone => "Milestone Completion".to_string(),
+            ServiceValueType::Full => "Upon Completion".to_string(),
+        }
+    }
+}
+
+impl From<ServiceValueType> for i16 {
+    fn from(t: ServiceValueType) -> Self {
+        match t {
+            ServiceValueType::Hourly => 0,
+            ServiceValueType::Milestone => 1 ,
+            ServiceValueType::Full => 2 
+        }
+    }
+}
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type, EnumIter)]
+pub enum ServiceItemType {
+    Service,
+    Item,
+}
+
+impl ToString for ServiceItemType {
+    fn to_string(&self) -> String {
+        match self {
+            ServiceItemType::Service => "Service".to_string(),
+            ServiceItemType::Item => "Item".to_string(),
+        }
+    }
+}
+
+impl Into<ServiceItemType> for i16 {
+    fn into(self) -> ServiceItemType {
+        match self {
+            0 => ServiceItemType::Service,
+            1 => ServiceItemType::Item, 
+            _ => ServiceItemType::Service
+        }
+    }
+}
+
+impl From<ServiceItemType> for i16 {
+    fn from(t: ServiceItemType) -> Self {
+        match t {
+            ServiceItemType::Service => 0,
+            ServiceItemType::Item => 1 , 
+        }
+    }
+} 
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct ServiceItem {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub external_accounting_id: String, 
+    pub owner_key: uuid::Uuid,
+    pub name: String,
+    pub description: String, 
+    
+    pub value: i64,
+    pub currency: String,
+    
+    pub service_item_type: ServiceItemType,
+    pub service_value_type: ServiceValueType,
+    pub expenses: Vec::<uuid::Uuid>, 
+    
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl ServiceItem {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        external_accounting_id: String,
+        owner_key: uuid::Uuid,
+        name: String,
+        description: String,
+        value: i64,
+        currency: String,
+        service_item_type: ServiceItemType,
+        service_value_type: ServiceValueType,
+        expenses: Vec::<uuid::Uuid>, 
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            external_accounting_id,
+            owner_key,
+            name,
+            description,
+            value,
+            currency,
+            service_item_type,
+            service_value_type,
+            expenses,
+            created, 
+            updated,
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "service_item.html")]
+pub struct ServiceItemTemplate {
+    service_item: ServiceItem,
+    organization: Organization,
+    user: crate::user::User,
+}
+
+impl<'a> ServiceItemTemplate {
+    pub fn new(
+        service_item: ServiceItem,
+        organization: Organization,
+        user: crate::user::User,
+    ) -> Self {
+        return Self {
+            service_item,
+            organization,
+            user,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 471 - 0
src/task.rs

@@ -0,0 +1,471 @@
+use std::str::FromStr;
+
+use askama::Template;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use sqlx::postgres::PgQueryResult;
+use strum::IntoEnumIterator;
+use strum_macros::EnumIter;
+use uuid::Uuid;
+
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::{State, user};
+use crate::matrix::post_task_create;
+ 
+// SQL STUFF
+
+pub async fn get_task( conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Task {
+    let task = sqlx::query!(
+        "select key, owner_key, organization_key, project_key, assignee_key, name, description, tags, status, estimated_quarter_days, start, due, created, updated from tasks where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await
+    .expect("Select task by key");
+
+    let t_status: TaskStatus = task.status.expect("status exists").into();
+    Task {
+        key: task.key.expect("key exists"),
+        organization_key: task.organization_key.expect("organization_key"), 
+        project_key: task.project_key.expect("project_key"), 
+        owner_key: task.owner_key.expect("owner_key exists"),
+        assignee_key: task.assignee_key.expect("assignee_key exists"),
+        name: task.name.expect("name exists"),
+        description: task.description.expect("description exists"),
+        tags: task.tags.expect("tags exists"),
+        status: t_status,
+        estimated_quarter_days: task.estimated_quarter_days.expect("estimated_quarter_days exists"),
+        start: task.start.expect("start"),
+        due:task.due.expect("due"),
+        created: task.created.expect("created exists"),
+        updated: task.updated.expect("updated exists"),
+    }
+}
+pub async fn get_tasks_by_organization(conn: &mut PoolConnection<Postgres>,organization_key: uuid::Uuid) -> Vec<Task> {
+    let records = sqlx::query!(
+        "select key, owner_key, organization_key, project_key, assignee_key, name, description, tags, status, estimated_quarter_days, start, due, created, updated from tasks where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .unwrap_or_default();
+
+    let mut tasks = Vec::<Task>::new();
+    for task in records {
+        let t_status: TaskStatus = task.status.expect("status exists").into();
+        let tsk = Task {
+            key: task.key.expect("key exists"),
+            organization_key: task.organization_key.expect("organization_key"), 
+            project_key: task.project_key.expect("project_key"), 
+            owner_key: task.owner_key.expect("owner_key exists"),
+            assignee_key: task.assignee_key.expect("assignee_key exists"),
+            name: task.name.expect("name exists"),
+            description: task.description.expect("description exists"),
+            tags: task.tags.expect("tags exists"),
+            status: t_status,
+            estimated_quarter_days: task.estimated_quarter_days.expect("estimated_quarter_days exists"),
+            start: task.start.expect("start"),
+            due:task.due.expect("due"),
+            created: task.created.expect("created exists"),
+            updated: task.updated.expect("updated exists"),
+        };
+        tasks.push(tsk);
+    }
+    return tasks
+}
+pub async fn get_tasks_by_project(conn: &mut PoolConnection<Postgres>,project_key: uuid::Uuid) -> Vec<Task> {
+    let records = sqlx::query!(
+        "select key, owner_key, organization_key, project_key, assignee_key, name, description, tags, status, estimated_quarter_days, start, due, created, updated from tasks where project_key = $1",
+        project_key
+    )
+    .fetch_all(conn)
+    .await
+    .unwrap_or_default();
+
+    let mut tasks = Vec::<Task>::new();
+    for task in records {
+        let t_status: TaskStatus = task.status.expect("status exists").into();
+        let tsk = Task {
+            key: task.key.expect("key exists"),
+            organization_key: task.organization_key.expect("organization_key"), 
+            project_key: task.project_key.expect("project_key"), 
+            owner_key: task.owner_key.expect("owner_key exists"),
+            assignee_key: task.assignee_key.expect("assignee_key exists"),
+            name: task.name.expect("name exists"),
+            description: task.description.expect("description exists"),
+            tags: task.tags.expect("tags exists"),
+            status: t_status,
+            estimated_quarter_days: task.estimated_quarter_days.expect("estimated_quarter_days exists"),
+            start: task.start.expect("start"),
+            due:task.due.expect("due"),
+            created: task.created.expect("created exists"),
+            updated: task.updated.expect("updated exists"),
+        };
+        tasks.push(tsk);
+    }
+    return tasks
+}
+
+
+async fn delete_task( conn: &mut PoolConnection<Postgres>, key: uuid::Uuid, owner_key: uuid::Uuid) -> Result<PgQueryResult, sqlx::Error> {
+    return sqlx::query!("DELETE FROM tasks where owner_key=$1 AND key=$2", owner_key, key)
+    .execute( conn)
+    .await;
+}
+
+async fn insert_task( conn: &mut PoolConnection<Postgres>, new_task: &Task) {
+    sqlx::query!("INSERT INTO tasks (key, owner_key, organization_key, project_key, assignee_key, name, description, tags, status, estimated_quarter_days, start, due, created, updated) values($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", 
+        new_task.key,
+        new_task.owner_key,
+        new_task.organization_key,
+        new_task.project_key, 
+        new_task.assignee_key,
+        &new_task.name,
+        &new_task.description,
+        &new_task.tags,
+        new_task.status as i16,
+        new_task.estimated_quarter_days,
+        new_task.start,
+        new_task.due,
+        new_task.created,
+        new_task.updated,
+    )
+    .execute( conn)
+    .await
+    .expect("Insert Success");
+}
+
+async fn update_task(conn: &mut PoolConnection<Postgres>, task: &Task) {
+    sqlx::query!("UPDATE tasks SET 
+    organization_key=$2, project_key=$3, owner_key=$4, assignee_key=$5, name=$6, 
+    description=$7, tags=$8, status=$9, estimated_quarter_days=$10, start=$11, due=$12, 
+    updated=extract(epoch from now()) where key = $1",
+     task.key, task.organization_key, task.project_key, task.owner_key, task.assignee_key, task.name,task.description, 
+     task.tags, task.status as i16, task.estimated_quarter_days, task.start, task.due)
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// Route Stuff
+
+pub async fn delete(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("task_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let s_uuid = uuid::Uuid::from_str(key).expect("Task uuid parse");
+            match delete_task(&mut conn, s_uuid, u.key).await {
+                Ok(_) =>    Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build()),
+                Err(_) =>  Ok(tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build()),
+            }
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    match req.param("task_id") {
+        Ok(key) => {
+            let s_uuid = uuid::Uuid::from_str(key).expect("Task uuid parse");
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let task = get_task(&mut conn, s_uuid).await;
+            let users = crate::user::get_users_by_organization(&mut conn, u.organization_key).await;
+            
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(
+                    TaskTemplate::new(
+                        task,
+                        u,
+                        users,
+                    )
+                    .render_string(),
+                )
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn insert(mut req: Request<State>) -> tide::Result {
+    let umd: Result<Task, tide::Error> = req.body_json().await;
+
+    let claims: user::UserJwtState = match user::read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        },
+    };
+     
+    match umd {
+        Ok(task) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            if task.key == uuid::Uuid::nil() {
+                let s = Task::new(task.organization_key, task.project_key, task.owner_key, task.assignee_key, task.name, task.description, task.tags,TaskStatus::Todo, task.estimated_quarter_days, task.start, task.due);
+                insert_task(&mut conn, &s).await;
+                let organization_key = uuid::Uuid::from_str(claims.organization_key.as_str()).expect("organization key");
+                post_task_create(&mut conn, claims.matrix_home_server, claims.matrix_user_id, organization_key, claims.matrix_access_token, &s).await.expect("Posting to matrix");
+                let j = serde_json::to_string(&s).expect("To JSON");
+                return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                    .content_type(mime::JSON)
+                    .body(j)
+                    .build());
+            }
+
+            update_task(&mut conn, &task).await;
+            let j = serde_json::to_string(&task).expect("To JSON");
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+// data types
+#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Copy, sqlx::Type, EnumIter)]
+pub enum TaskStatus {
+    Wishlist,
+    Todo,
+    PlanningAndEstimating,
+    InQueue,
+    InProgress,
+    ToReview,
+    InReviewal,
+    Complete,
+}
+impl Into<TaskStatus> for i16 {
+    fn into(self) -> TaskStatus {
+        match self {
+            0 => TaskStatus::Wishlist,
+            1 => TaskStatus::Todo, 
+            2 => TaskStatus::PlanningAndEstimating, 
+            3 => TaskStatus::InQueue, 
+            4 => TaskStatus::InProgress, 
+            5 => TaskStatus::ToReview, 
+            6 => TaskStatus::InReviewal, 
+            7 => TaskStatus::Complete, 
+            _ => TaskStatus::Wishlist
+        }
+    }
+}
+
+ 
+impl FromStr for TaskStatus {
+    type Err = ();
+    fn from_str(input: &str) -> Result<TaskStatus, Self::Err> {
+        match input {
+            "Wishlist" => Ok(TaskStatus::Wishlist),
+            "Todo" => Ok(TaskStatus::Todo),
+            "PlanningAndEstimating" => Ok(TaskStatus::PlanningAndEstimating),
+            "InQueue" => Ok(TaskStatus::InQueue),
+            "InProgress" => Ok(TaskStatus::InProgress),
+            "ToReview" => Ok(TaskStatus::ToReview),
+            "InReviewal" => Ok(TaskStatus::InReviewal),
+            "Complete" => Ok(TaskStatus::Complete),
+            _ => Ok(TaskStatus::Wishlist),
+        }
+    }
+}
+
+impl ToString for TaskStatus {
+    fn to_string(&self) -> String {
+        match self {
+            TaskStatus::Wishlist => "Wishlist".to_owned(),
+            TaskStatus::Todo => "Todo".to_owned(),
+            TaskStatus::PlanningAndEstimating => "Planning And Estimating".to_owned(),
+            TaskStatus::InQueue => "In Queue".to_owned(),
+            TaskStatus::InProgress => "In Progress".to_owned(),
+            TaskStatus::ToReview =>"To Review".to_owned(),
+            TaskStatus::InReviewal => "In Reviewal".to_owned(),
+            TaskStatus::Complete => "Complete".to_owned(),
+        }
+    }
+}
+
+impl From<TaskStatus> for i16 {
+    fn from(t: TaskStatus) -> Self {
+        match t {
+            TaskStatus::Wishlist => 0,
+            TaskStatus::Todo => 1, 
+            TaskStatus::PlanningAndEstimating => 2,
+            TaskStatus::InQueue => 3,
+            TaskStatus::InProgress => 4,
+            TaskStatus::ToReview => 5,
+            TaskStatus::InReviewal => 6,
+            TaskStatus::Complete => 7,
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Task {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub project_key: uuid::Uuid,
+    pub owner_key: uuid::Uuid,
+    pub assignee_key: uuid::Uuid,
+    pub name: String,
+    pub description: String,
+    pub tags: String,
+    pub status: TaskStatus,
+    pub estimated_quarter_days: i32,
+    pub start: i64,
+    pub due: i64,
+    pub created: i64,
+    pub updated: i64,
+}
+ 
+impl Task {
+    pub fn new(
+        organization_key: uuid::Uuid,
+        project_key: uuid::Uuid,
+        owner_key: uuid::Uuid,
+        assignee_key: uuid::Uuid,
+        name: String,
+        description: String,
+        tags: String,
+        status: TaskStatus,
+        estimated_quarter_days:i32,
+        start: i64,
+        due: i64,
+    ) -> Self {
+        let key = Uuid::new_v4();
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0; 
+        Self {
+            key,
+            organization_key,
+            project_key,
+            owner_key,
+            assignee_key,
+            name,
+            description,
+            tags,
+            status,
+            estimated_quarter_days,
+            start,
+            due,
+            created, 
+            updated,
+        }
+    }
+}
+
+pub async fn add(req: Request<State>) -> tide::Result {
+    
+    let u = match crate::user::user_or_error(&req) {
+        Ok(value) => value,
+        Err(_) => return Ok(tide::Redirect::new("/login").into()),
+    };
+    
+    match req.param("project_id") {
+        Ok(project_id) => {  
+            let project_id = uuid::Uuid::from_str(project_id).expect("Project uuid parse");
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            let users = crate::user::get_users_by_organization(&mut conn, u.organization_key).await;
+            let mut task= Task::new( u.organization_key, project_id,  u.key,  uuid::Uuid::nil(),  "".to_owned(), "".to_owned(), "".to_owned(), TaskStatus::Todo,  0, 0, 0);
+            task.key = uuid::Uuid::nil();
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+            .content_type(mime::HTML)
+            .body(
+                TaskTemplate::new(
+                    task,
+                    u,
+                    users,
+                )
+                .render_string(),
+            )
+            .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "task.html")]
+pub struct TaskTemplate {
+    task: Task,
+    user: crate::user::User,
+    users: Vec::<crate::user::User>,
+}
+
+impl<'a> TaskTemplate {
+    pub fn new(
+        task: Task,
+        user: crate::user::User,
+        users: Vec::<crate::user::User>,
+    ) -> Self {
+        return Self {
+            task,
+            user,
+            users,
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}

+ 914 - 0
src/user.rs

@@ -0,0 +1,914 @@
+use std::str::FromStr;
+use std::time::SystemTime;
+
+use askama::Template;
+use chrono::DateTime;
+use chrono::Utc;
+use jsonwebtoken::TokenData;
+use matrix_sdk::ruma::device_id;
+use matrix_sdk::ruma::OwnedUserId;
+use matrix_sdk::Session;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Postgres;
+use uuid::Uuid;
+
+use tide::http::cookies::Cookie;
+use tide::Response;
+use tide::StatusCode;
+use tide::{http::mime, Request};
+
+use crate::home::NotFoundTemplate;
+use crate::matrix;
+use crate::matrix::Choice;
+use crate::organization;
+use crate::organization::Organization;
+use crate::State;
+use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct UserJwtState {
+    pub key: String,
+    pub organization_key: String,
+    pub email: String,
+    pub created: i64,
+
+    pub matrix_user_id: String,
+    pub matrix_access_token: String,
+    pub matrix_device_id: String,
+    pub matrix_refresh_token: String,
+    pub matrix_home_server: String,
+
+    pub updated: i64,
+    exp: i64,
+}
+
+pub async fn get_users_by_organization(
+    conn: &mut PoolConnection<Postgres>,
+    organization_key: uuid::Uuid,
+) -> Vec<User> {
+    let user_records = sqlx::query!(
+        "select key, organization_key, email,  matrix_user_id, matrix_home_server, created, updated from users where organization_key = $1",
+        organization_key
+    )
+    .fetch_all(conn)
+    .await
+    .expect("Select user by email");
+    let mut users = vec![];
+    for user in user_records {
+        users.push(User {
+            key: user.key.expect("key exists"),
+            organization_key: user.organization_key.expect("key exists"),
+            email: user.email.expect("email exists"),
+            matrix_user_id: user.matrix_user_id.expect("matrix_user_id exists"),
+            matrix_home_server: user.matrix_home_server.expect("matrix_home_server exists"),
+            created: user.created.expect("created exists"),
+            updated: user.updated.expect("updated exists"),
+        })
+    }
+    users
+}
+
+pub async fn get_user(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) -> Option<User> {
+    match sqlx::query!(
+        "select key, organization_key, email, matrix_user_id, matrix_home_server, created, updated from users where key = $1",
+        key
+    )
+    .fetch_one(conn)
+    .await {
+        Ok(user) => Some(User {
+            key: user.key.expect("key exists"),
+            organization_key: user.organization_key.expect("key exists"),
+            email: user.email.expect("email exists"),
+            matrix_user_id: user.matrix_user_id.expect("matrix_user_id exists"),
+            matrix_home_server: user.matrix_home_server.expect("matrix_home_server exists"),
+            created: user.created.expect("created exists"),
+            updated: user.updated.expect("updated exists"),
+        }),
+        Err(_) => { None }
+    }
+}
+
+pub async fn get_user_by_matrix_user_id(
+    conn: &mut PoolConnection<Postgres>,
+    matrix_user_id: String,
+) -> Option<User> {
+    match sqlx::query!(
+        "select key, organization_key, email, matrix_user_id, matrix_home_server, created, updated from users where matrix_user_id = $1",
+        &matrix_user_id
+    )
+    .fetch_one(conn)
+    .await {
+        Ok(user) => {
+            Some(User {
+                key: user.key.expect("key exists"),
+                organization_key: user.organization_key.expect("key exists"),
+                email: user.email.expect("email exists"),
+                matrix_user_id: user.matrix_user_id.expect("matrix_user_id exists"),
+                matrix_home_server: user.matrix_home_server.expect("matrix_home_server exists"),
+                created: user.created.expect("created exists"),
+                updated: user.updated.expect("updated exists"),
+            })
+        }, Err(_) => {
+            None
+        }
+    }
+}
+
+async fn delete_user(conn: &mut PoolConnection<Postgres>, key: uuid::Uuid) {
+    sqlx::query!("DELETE FROM users where key=$1", key)
+        .execute(conn)
+        .await
+        .expect("Insert Success");
+}
+
+async fn insert_user(conn: &mut PoolConnection<Postgres>, new_user: &User) {
+    sqlx::query!("INSERT INTO users (key, organization_key, email, matrix_user_id, matrix_home_server, created, updated) values($1, $2, $3, $4, $5, $6, $7)", new_user.key, new_user.organization_key, &new_user.email, &new_user.matrix_user_id, &new_user.matrix_home_server, new_user.created, new_user.updated)
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+async fn update_user(conn: &mut PoolConnection<Postgres>, user: &User) {
+    sqlx::query!("UPDATE users SET organization_key=$2, email=$3, matrix_user_id=$4, matrix_home_server=$5, updated=extract(epoch from now()) where key = $1", user.key, user.organization_key, &user.email, &user.matrix_user_id, &user.matrix_home_server)
+    .execute(conn)
+    .await
+    .expect("Insert Success");
+}
+
+// JWT AUTHENTICATION STUFF
+pub fn jwt_valid(jwt: String) -> Result<TokenData<UserJwtState>, jsonwebtoken::errors::Error> {
+    let token_part = jwt.strip_prefix("token=").expect("token part removed");
+    let jwt_secret = std::env::var("JWT_SECRET")
+        .expect("Missing `JWT_SECRET` env variable, needed for running the server");
+    return decode::<UserJwtState>(
+        &token_part,
+        &DecodingKey::from_secret(jwt_secret.as_bytes()),
+        &Validation::new(Algorithm::HS512),
+    )
+    .map_err(|e| {
+        println!("{}", e);
+        return e;
+    });
+}
+
+pub fn create_jwt(
+    u: &User,
+    matrix_access_token: String,
+    matrix_device_id: String,
+    matrix_refresh_token: String,
+) -> Result<String, jsonwebtoken::errors::Error> {
+    let expiration = chrono::Utc::now()
+        .checked_add_signed(chrono::Duration::hours(2))
+        .expect("valid timestamp")
+        .timestamp();
+    let jwt_state = UserJwtState {
+        key: u.key.to_string(),
+        organization_key: u.organization_key.to_string(),
+        email: u.email.to_string(),
+        matrix_user_id: u.matrix_user_id.clone(),
+        matrix_home_server: u.matrix_home_server.clone(),
+        matrix_access_token: matrix_access_token,
+        matrix_device_id: matrix_device_id,
+        matrix_refresh_token: matrix_refresh_token,
+        created: u.created,
+        updated: u.updated,
+        exp: expiration,
+    };
+    let jwt_secret = std::env::var("JWT_SECRET")
+        .expect("Missing `JWT_SECRET` env variable, needed for running the server");
+    let header = Header::new(Algorithm::HS512);
+    encode(
+        &header,
+        &jwt_state,
+        &EncodingKey::from_secret(jwt_secret.as_bytes()),
+    )
+    .map_err(|e| e)
+}
+
+pub fn read_jwt_cookie(cookie: Option<Cookie<'static>>) -> Option<UserJwtState> {
+    let cookie_val = match cookie {
+        Some(c) => c,
+        None => return None,
+    };
+
+    let valid = match jwt_valid(cookie_val.to_string()) {
+        Ok(t) => t,
+        Err(_e) => return None,
+    };
+    return Some(valid.claims);
+}
+
+pub fn read_jwt_cookie_to_user(cookie: Option<Cookie<'static>>) -> Option<User> {
+    let cookie_val = match cookie {
+        Some(c) => c,
+        None => return None,
+    };
+
+    let valid = match jwt_valid(cookie_val.to_string()) {
+        Ok(t) => t,
+        Err(_e) => return None,
+    };
+    let u = User::new(
+        uuid::Uuid::from_str(valid.claims.key.as_str()).expect("parse cookie"),
+        uuid::Uuid::from_str(&valid.claims.organization_key)
+            .expect("parse organization key from jwt"),
+        valid.claims.email,
+        valid.claims.matrix_user_id,
+        valid.claims.matrix_home_server,
+    );
+
+    return Some(u);
+}
+// Route Stuff
+pub async fn delete(req: Request<State>) -> tide::Result {
+    if let Some(value) = jwt_invalid(&req) {
+        return value;
+    }
+
+    match req.param("user_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?;
+            let u_uuid = uuid::Uuid::from_str(key).expect("User uuid parse");
+            delete_user(&mut conn, u_uuid).await;
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub fn user_jwt_state_invalid(claims: UserJwtState) -> Option<Result<Response, tide::Error>> {
+    let now = chrono::Utc::now().timestamp();
+    if claims.exp < now {
+        return Some(Ok(tide::Response::builder(tide::StatusCode::Unauthorized)
+            .content_type(mime::PLAIN)
+            .body("EXPIRED")
+            .build()));
+    }
+    None
+}
+
+pub fn jwt_invalid(req: &Request<State>) -> Option<Result<Response, tide::Error>> {
+    let claims = match read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Some(Ok(tide::Response::builder(tide::StatusCode::Unauthorized)
+                .content_type(mime::PLAIN)
+                .body("UNAUTHORIZED")
+                .build()))
+        }
+    };
+    let now = chrono::Utc::now().timestamp();
+    if claims.exp < now {
+        return Some(Ok(tide::Response::builder(tide::StatusCode::Unauthorized)
+            .content_type(mime::PLAIN)
+            .body("EXPIRED")
+            .build()));
+    }
+    None
+}
+
+pub fn user_or_error(req: &Request<State>) -> Result<User, Result<tide::Response, tide::Error>> {
+    let claims = match read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Err(Ok(tide::Response::builder(tide::StatusCode::Unauthorized)
+                .content_type(mime::PLAIN)
+                .body("UNAUTHORIZED")
+                .build()))
+        }
+    };
+    if let Some(value) = user_jwt_state_invalid(claims.clone()) {
+        return Err(value);
+    }
+    let u = User {
+        key: uuid::Uuid::from_str(claims.key.as_str()).expect("key parses"),
+        organization_key: uuid::Uuid::from_str(claims.organization_key.as_str())
+            .expect("key parses"),
+        email: claims.email,
+        matrix_user_id: claims.matrix_user_id,
+        matrix_home_server: claims.matrix_home_server,
+        created: claims.created,
+        updated: claims.updated,
+    };
+    Ok(u)
+}
+
+pub async fn get(req: Request<State>) -> tide::Result {
+    if let Some(value) = jwt_invalid(&req) {
+        return value;
+    }
+
+    match req.param("user_id") {
+        Ok(key) => {
+            let mut conn = req.state().db_pool.acquire().await?; // .await? needs to be a real connection pool error handler!!!!!!!!
+            let u_uuid = uuid::Uuid::from_str(key).expect("User uuid parse");
+            let user = get_user(&mut conn, u_uuid).await.expect("user exists");
+
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(UserTemplate::new(&user, user.email.as_str()).render_string())
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::NotFound)
+                .content_type(mime::HTML)
+                .body(NotFoundTemplate::new().render_string())
+                .build())
+        }
+    }
+}
+
+pub async fn register(_req: Request<State>) -> tide::Result {
+    let auth = "matrix";
+    if auth == "matrix" {
+        let homeserver_url = "https://matrix-client.matrix.org";
+        let choices = matrix::get_login_urls(
+            homeserver_url.to_string(),
+            "http://localhost:8080/register_matrix".to_string(),
+        )
+        .await
+        .expect("Get login choices");
+        let register = RegisterTemplate::new(choices);
+        return Ok(tide::Response::builder(tide::StatusCode::Ok)
+            .content_type(mime::HTML)
+            .body(register.render_string())
+            .build());
+    }
+    let register = RegisterTemplate::new(vec![]);
+    Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(register.render_string())
+        .build())
+}
+
+pub async fn update(mut req: Request<State>) -> tide::Result {
+    let _claims: UserJwtState = match read_jwt_cookie(req.cookie("token")) {
+        Some(c) => c,
+        None => {
+            return Ok(tide::Redirect::new("/login").into());
+        }
+    };
+    let umd: Result<User, tide::Error> = req.body_json().await;
+    match umd {
+        Ok(u) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            update_user(&mut conn, &u).await;
+            let j = serde_json::to_string(&u).expect("To JSON");
+            Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::JSON)
+                .body(j)
+                .build())
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+pub async fn register_post(req: Request<State>) -> tide::Result {
+    let vals = req.url().query().expect("Get query values");
+    let login_token = vals.split("loginToken=").last().expect("Token found");
+    let homeserver_url = "https://matrix-client.matrix.org".to_string();
+    let matrix_login_response =
+        crate::matrix::login_with_token(homeserver_url.clone(), login_token.to_string()).await;
+
+    match matrix_login_response {
+        Ok(u) => {
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+            let (email, avatar, display_name)  = crate::matrix::account(homeserver_url.clone(), login_token.to_string(), u.user_id.clone()).await;
+            let organization_key = Uuid::new_v4();
+            let key = Uuid::new_v4();
+            let new_user = User::new(
+                key,
+                organization_key,
+                email,
+                u.user_id.to_string(),
+                u.home_server.expect("Get home server").to_string(),
+            );
+
+            organization::insert_organization(
+                &mut conn,
+                &Organization::new(
+                    organization_key,
+                    "".to_string(),
+                    "".to_string(),
+                    new_user.key,
+                    "Welcome Inc.".to_string(),
+                    "".to_string(),
+                    homeserver_url,
+                    "".to_string(),
+                    "".to_string(),
+                    "".to_string(),
+                    "".to_string(),
+                ),
+            )
+            .await;
+
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            insert_user(&mut conn, &new_user).await;
+            let mut res = Response::new(StatusCode::TemporaryRedirect);
+            let now = SystemTime::now();
+            let cadd = now
+                .checked_add(u.expires_in.unwrap_or_default())
+                .expect("added");
+            let datetime: DateTime<Utc> = cadd.into();
+
+            let jwt = create_jwt(&new_user, "".to_string(), "".to_string(), "".to_string())
+                .expect("JWT Created");
+
+            let cookie_str = format!("token={}; Expires={}", jwt, datetime.to_string());
+            // let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT";
+            let c = Cookie::parse(cookie_str).unwrap();
+            res.insert_cookie(c);
+            res.insert_header("Location", "/dashboard");
+            Ok(res)
+        }
+        Err(e) => {
+            println!("{:?}", e);
+            Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::JSON)
+                .body("{'error': 'invalid json body'}")
+                .build())
+        }
+    }
+}
+
+pub async fn logout(_req: Request<State>) -> tide::Result {
+    let mut res = Response::new(StatusCode::TemporaryRedirect);
+    let cookie_str = format!("token={};", "");
+    // let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT";
+    let c = Cookie::parse(cookie_str).unwrap();
+    res.remove_cookie(c);
+    res.insert_header("Location", "/");
+    Ok(res)
+}
+
+pub async fn login_by_username(req: Request<State>) -> tide::Result {
+    let homeserver_url = "https://matrix-client.matrix.org";
+    let claims: crate::user::UserJwtState = match crate::user::read_jwt_cookie(req.cookie("token"))
+    {
+        Some(c) => c,
+        None => {
+            // JWT probably expired
+            let login = LoginUsernameTemplate::new();
+            return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(login.render_string())
+                .build());
+        }
+    };
+    // lets refresh and go
+    // fresh login
+    let login = LoginUsernameTemplate::new();
+    return Ok(tide::Response::builder(tide::StatusCode::Ok)
+        .content_type(mime::HTML)
+        .body(login.render_string())
+        .build());
+}
+
+pub async fn login(req: Request<State>) -> tide::Result {
+    let homeserver_url = "https://matrix-client.matrix.org";
+    let claims: crate::user::UserJwtState = match crate::user::read_jwt_cookie(req.cookie("token"))
+    {
+        Some(c) => c,
+        None => {
+            // JWT probably expired
+            let choices = matrix::get_login_urls(
+                homeserver_url.to_string(),
+                "http://localhost:8080/login_matrix".to_string(),
+            )
+            .await
+            .expect("Get login choices");
+            let login = LoginTemplate::new(choices);
+            return Ok(tide::Response::builder(tide::StatusCode::Ok)
+                .content_type(mime::HTML)
+                .body(login.render_string())
+                .build());
+        }
+    };
+    // lets refresh and go
+    if claims.matrix_refresh_token.len() > 0 {
+        let user_id =
+            <OwnedUserId>::try_from(claims.matrix_user_id.as_str()).expect("parse user id");
+        let session = Session {
+            access_token: claims.matrix_access_token,
+            refresh_token: Some(claims.matrix_refresh_token),
+            user_id,
+            device_id: device_id!("kinbrio").to_owned(),
+        };
+        crate::matrix::restore_from_session(claims.matrix_home_server, session)
+            .await
+            .expect("Restoring from session");
+        return Ok(tide::Redirect::new("/").into());
+    } else {
+        // fresh login
+        let choices = matrix::get_login_urls(
+            homeserver_url.to_string(),
+            "http://localhost:8080/login_matrix".to_string(),
+        )
+        .await
+        .expect("Get login choices");
+        let login = LoginTemplate::new(choices);
+        return Ok(tide::Response::builder(tide::StatusCode::Ok)
+            .content_type(mime::HTML)
+            .body(login.render_string())
+            .build());
+    }
+}
+
+pub async fn login_matrix(req: Request<State>) -> tide::Result {
+    let vals = req.url().query().expect("Get query values");
+    let login_token = vals.split("loginToken=").last().expect("Token found");
+    let matrix_login_response = match crate::matrix::login_with_token(
+        "https://matrix-client.matrix.org".to_string(),
+        login_token.to_string(),
+    )
+    .await
+    {
+        Ok(mls) => mls,
+        Err(_e) => {
+            return Ok(tide::Redirect::new("/register").into());
+        }
+    };
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    let matrix_user_id = matrix_login_response.user_id.to_string();
+    let matrix_access_token = matrix_login_response.access_token;
+    let matrix_device_id = matrix_login_response.device_id.to_string();
+    let matrix_refresh_token = matrix_login_response.refresh_token.unwrap_or_default();
+    let matrix_expires_in = matrix_login_response.expires_in.unwrap_or_default();
+    let matrix_home_server = matrix_login_response
+        .home_server
+        .as_ref()
+        .expect("Get home serve");
+
+    // TODO: add access token/refresh/etc to redis with the matrix_expires_in
+    let user = match get_user_by_matrix_user_id(&mut conn, matrix_user_id.clone()).await {
+        Some(u) => u,
+        None => {
+            return Ok(tide::Redirect::new("/register").into());
+        }
+    };
+
+    let mut res = Response::new(StatusCode::TemporaryRedirect);
+
+    let user_t = User {
+        key: user.key,
+        organization_key: user.organization_key,
+        email: user.email,
+
+        matrix_user_id: matrix_user_id.clone(),
+        matrix_home_server: matrix_home_server.to_string().clone(),
+
+        created: user.created,
+        updated: user.updated,
+    };
+    let jwt = create_jwt(
+        &user_t,
+        matrix_access_token,
+        matrix_device_id,
+        matrix_refresh_token.to_string(),
+    )
+    .expect("JWT Created");
+    let now = SystemTime::now();
+    let cadd = now.checked_add(matrix_expires_in).expect("added");
+    let datetime: DateTime<Utc> = cadd.into();
+
+    let cookie_str = format!("token={}; Expires={}", jwt, datetime.to_string());
+    let c = Cookie::parse(cookie_str).unwrap();
+    res.insert_cookie(c);
+    res.insert_header("Location", "/dashboard");
+    Ok(res)
+}
+
+pub async fn login_post(mut req: Request<State>) -> tide::Result {
+    let u: LoginPostRequest = match req.body_json().await {
+        Ok(pr) => pr,
+        Err(e) => {
+            return Ok(tide::Response::builder(tide::StatusCode::BadRequest)
+                .content_type(mime::PLAIN)
+                .body(e.to_string())
+                .build())
+        }
+    };
+
+    let mut conn = match req.state().db_pool.acquire().await {
+        Ok(c) => c,
+        Err(e) => {
+            return Ok(
+                tide::Response::builder(tide::StatusCode::InternalServerError)
+                    .content_type(mime::PLAIN)
+                    .body(e.to_string())
+                    .build(),
+            )
+        }
+    };
+    let homeserver = u.homeserver.clone();
+    let matrix_login_response = match crate::matrix::login_with_password(
+        homeserver.clone(),
+        u.uid.to_string(),
+        u.secret.to_string(),
+    )
+    .await
+    {
+        Ok(v) => v,
+        Err(_) => {
+            return Ok(tide::Redirect::new("/login_by_username").into());
+        }
+    };
+    if matrix_login_response.access_token.len() == 0 {
+        return Ok(
+            tide::Response::builder(tide::StatusCode::InternalServerError)
+                .content_type(mime::PLAIN)
+                .body("Unexpected errors from matrix loggin - no valid access token returned")
+                .build(),
+        );
+    }
+    let (email, avatar, display_name)  = crate::matrix::account(homeserver.clone(), matrix_login_response.access_token.clone(), matrix_login_response.user_id.clone()).await;
+    
+    let user = match get_user_by_matrix_user_id(&mut conn, matrix_login_response.user_id.to_string())
+    .await {
+        Some(u) => u,
+        None => {
+           
+            let organization_key = Uuid::new_v4();
+            let key = Uuid::new_v4();
+            let new_user: User = User::new(
+                key,
+                organization_key,
+                email,
+                matrix_login_response.user_id.to_string(),
+                homeserver.clone(),
+            );
+            
+            organization::insert_organization(
+                &mut conn,
+                &Organization::new(
+                    organization_key,
+                    "".to_string(),
+                    "".to_string(),
+                    new_user.key,
+                    "Welcome Inc.".to_string(),
+                    "".to_string(),
+                    homeserver.clone(),
+                    "".to_string(),
+                    "".to_string(),
+                    "".to_string(),
+                    "".to_string(),
+                ),
+            )
+            .await;
+
+            let mut conn = match req.state().db_pool.acquire().await {
+                Ok(c) => c,
+                Err(e) => {
+                    return Ok(
+                        tide::Response::builder(tide::StatusCode::InternalServerError)
+                            .content_type(mime::PLAIN)
+                            .body(e.to_string())
+                            .build(),
+                    )
+                }
+            };
+
+            insert_user(&mut conn, &new_user).await;
+            new_user
+        }
+    };
+    let mut res = Response::new(StatusCode::Ok);
+     
+    let jwt =
+        create_jwt(&user, "".to_string(), "".to_string(), "".to_string()).expect("JWT Created");
+
+    let now = SystemTime::now();
+    let cadd = now;
+    let datetime: DateTime<Utc> = cadd.into();
+    let cookie_str = format!("token={}; Expires={}", jwt, datetime.to_string());
+    let c = Cookie::parse(cookie_str).unwrap();
+    res.insert_cookie(c);
+    Ok(res)
+}
+
+#[derive(Template)]
+#[template(path = "user.html")]
+pub struct UserTemplate<'a> {
+    user: &'a User,
+    email: &'a str,
+}
+
+impl<'a> UserTemplate<'a> {
+    pub fn new(user: &'a User, email: &'a str) -> Self {
+        return Self { user, email };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Deserialize)]
+pub struct LoginPostRequest {
+    pub uid: String,
+    pub secret: String,
+    pub homeserver: String,
+}
+
+#[derive(Template)]
+#[template(path = "login_username.html")]
+
+pub struct LoginUsernameTemplate {
+    user: User,
+}
+
+impl<'a> LoginUsernameTemplate {
+    pub fn new() -> Self {
+        return Self {
+            user: User {
+                key: uuid::Uuid::nil(),
+                organization_key: uuid::Uuid::nil(),
+                email: "".to_string(),
+                matrix_home_server: "".to_string(),
+                matrix_user_id: "".to_string(),
+                created: 0,
+                updated: 0,
+            },
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "login.html")]
+pub struct LoginTemplate {
+    user: User,
+    choices: Vec<Choice>,
+}
+
+impl<'a> LoginTemplate {
+    pub fn new(choices: Vec<Choice>) -> Self {
+        return Self {
+            choices: choices,
+            user: User {
+                key: uuid::Uuid::nil(),
+                organization_key: uuid::Uuid::nil(),
+                email: "".to_string(),
+                matrix_home_server: "".to_string(),
+                matrix_user_id: "".to_string(),
+                created: 0,
+                updated: 0,
+            },
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Template)]
+#[template(path = "register.html")]
+pub struct RegisterTemplate {
+    user: User,
+    choices: Vec<Choice>,
+}
+
+impl<'a> RegisterTemplate {
+    pub fn new(choices: Vec<Choice>) -> Self {
+        return Self {
+            choices: choices,
+            user: User {
+                key: uuid::Uuid::nil(),
+                organization_key: uuid::Uuid::nil(),
+                email: "".to_string(),
+                matrix_home_server: "".to_string(),
+                matrix_user_id: "".to_string(),
+                created: 0,
+                updated: 0,
+            },
+        };
+    }
+
+    pub fn render_string(&self) -> String {
+        return self.render().unwrap();
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct RegisterUser {
+    pub email: String,
+    pub organization: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct LoginData {
+    email: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct UserPreferences {
+    pub key: uuid::Uuid,
+    pub user_key: uuid::Uuid,
+    pub email: String,
+
+    pub matrix_user_id: String,
+    pub matrix_home_server: String,
+    pub created: i64,
+    pub updated: i64,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct User {
+    pub key: uuid::Uuid,
+    pub organization_key: uuid::Uuid,
+    pub email: String,
+    pub matrix_user_id: String,
+    pub matrix_home_server: String,
+    pub created: i64,
+    pub updated: i64,
+}
+
+impl ToString for User {
+    fn to_string(&self) -> String {
+        format!("{}", self.email)
+    }
+}
+
+impl User {
+    pub fn new(
+        key: uuid::Uuid,
+        organization_key: uuid::Uuid,
+        email: String,
+
+        matrix_user_id: String,
+        matrix_home_server: String,
+    ) -> Self {
+        let created = chrono::Utc::now().timestamp();
+        let updated = 0;
+        Self {
+            key,
+            organization_key,
+            email,
+            matrix_user_id,
+            matrix_home_server,
+            created,
+            updated,
+        }
+    }
+}

+ 6 - 0
start-gpt.sh

@@ -0,0 +1,6 @@
+# run.sh can use --with-cuda or the model param as 7b for smaller machines
+# it will take a minute to "come online" clean run it's downloading a few GB of model
+#!/bin/bash
+cd kinbot/kinbot-gpt && sudo ./run.sh --model 13b --with-cuda &
+
+wait

+ 7 - 0
start-matrix-bot.sh

@@ -0,0 +1,7 @@
+# run.sh can use --with-cuda or the model param as 7b for smaller machines
+# it will take a minute to "come online" clean run it's downloading a few GB of model
+
+
+$(cd kinbot/matrix_bot && cargo run &)
+
+wait

+ 235 - 0
tables.sql

@@ -0,0 +1,235 @@
+CREATE TABLE IF NOT EXISTS organization (
+    key uuid,
+    external_accounting_id text,
+    external_accounting_url text,
+    owner_key uuid,
+    domain varchar(2048),
+    contact_email varchar(320),
+    name varchar(256),
+    description varchar(512),
+    matrix_home_server text,
+    matrix_live_support_room_url text,
+    matrix_general_room_url text,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS users (
+    key uuid,
+    organization_key uuid,
+    email varchar(320),
+    matrix_user_id varchar(512),
+    matrix_home_server text,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS files (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    association_type smallint,
+    association_key uuid,
+    url varchar(2048),
+    hash text,
+    name varchar(256),
+    description varchar(512),
+    tags varchar(256),
+    format varchar(32),
+    size bigint,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS notes (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    association_type smallint,
+    association_key uuid,
+    url varchar(2048),
+    hash text,
+    title varchar(256),
+    content varchar(512),
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS service_items (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    external_accounting_id text,
+    name varchar(256),
+    description varchar(512),
+    value bigint,
+    currency varchar(16),
+    service_item_type smallint,
+    service_value_type smallint,
+    expenses uuid [],
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS projects (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    name varchar(256),
+    description varchar(512),
+    tags varchar(256),
+    estimated_quarter_days int,
+    start bigint,
+    due bigint,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS mile_stones (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    project_key uuid,
+    name varchar(256),
+    description varchar(512),
+    tags varchar(256),
+    estimated_quarter_days int,
+    start bigint,
+    due bigint,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS tasks (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    project_key uuid,
+    assignee_key uuid,
+    name varchar(256),
+    description varchar(512),
+    tags varchar(256),
+    status smallint,
+    estimated_quarter_days int,
+    start bigint,
+    due bigint,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS boards (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    project_key uuid,
+    name varchar(256),
+    description varchar(512),
+    columns text [],
+    lanes text [],
+    filter text,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS entitys (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    external_accounting_id text,
+    name varchar(256),
+    description varchar(512),
+    matrix_room_url text,
+    web_url text,
+    avatar_url text,
+    entity_type smallint,
+    address_primary text,
+    address_unit text,
+    city text,
+    state text,
+    zip_code text,
+    country text,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS contacts (
+    key uuid,
+    entity_key uuid,
+    organization_key uuid,
+    external_accounting_id text,
+    first_name varchar(128),
+    middle_initial varchar(12),
+    last_name varchar(128),
+    description varchar(512),
+    position varchar(512),
+    email varchar(320),
+    phone varchar(16),
+    secondary_email varchar(320),
+    secondary_phone varchar(16),
+    matrix_user_id varchar(512),
+    web_url text,
+    avatar_url text,
+    social_urls text [],
+    address_primary text,
+    address_unit text,
+    city text,
+    state text,
+    zip_code text,
+    country text,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS rooms (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+    name varchar(256),
+    description varchar(512),
+    matrix_room_url text,
+    matrix_room_id varchar(512),
+    message_types smallint,
+    alert_level smallint,
+    created bigint,
+    updated bigint
+);
+
+CREATE TABLE IF NOT EXISTS akaunting_options (
+    key uuid,
+    owner_key uuid,
+    organization_key uuid,
+
+    matrix_room_url text,
+    user_name text,
+    user_pass text,
+    akaunting_domain text,
+    akaunting_company_id text,
+
+    organization_data boolean,
+    employee_data boolean,
+    client_data boolean,
+    vendor_data boolean,
+    item_data boolean,
+    invoice_data boolean,
+    allow_post boolean,
+    last_sync bigint,
+    created bigint,
+    updated bigint
+);
+
+
+GRANT ALL ON ALL TABLES IN SCHEMA public TO projectmanager;
+
+-- delete from akaunting_options;
+-- delete from boards           ;
+-- delete from contacts         ;
+-- delete from entitys          ;
+-- delete from files            ;
+-- delete from mile_stones      ;
+-- delete from notes            ;
+-- delete from organization     ;
+-- delete from projects         ;
+-- delete from rooms            ;
+-- delete from service_items    ;
+-- delete from tasks            ;
+-- delete from users;

+ 178 - 0
templates/akaunting.html

@@ -0,0 +1,178 @@
+{% extends "layout.html" %}
+
+{% block title %}Akaunting Settings{% endblock %}
+{% block description %}Integrate and manage your Akaunting platform{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<script>
+    function set_company_id(val) { 
+        document.getElementById('akaunting_company_id').value = val
+    }
+    function import_item_id(id) { document.getElementById('akaunting_company_id').value = val }
+    function import_customer_id(id) { document.getElementById('akaunting_company_id').value = val }
+    
+</script>
+<div class="container">
+    <div class="section">
+        <div class="heading_div">
+            <h1>Keep your books in order. Run reports and projections. Organization you accounting. Build plugins. Host
+                it yourself</h1>
+            <h3>Don't Already Have Your Own Akaunting login?</h3>
+            <h4>The simplest way to register an account at <a target="_blank"
+                    href="https://akaunting.com">Akaunting.com</a>, a managed service.
+            </h4>
+            <h5>Once you have a valid username and password for your akaunting you can enter it in below to link your
+                kinbrio platform and synchronize your data!</h5>
+            <p>You can <a href="/documentation#setup-akaunting">host a private instance of akaunting</a></p>
+        </div>
+        <div class="row justified">
+            <div class="backed content">
+                <img decoding="async" loading="lazy" width="128px" height="128px"alt="Akaunting logo" src="/fs/images/akaunting-logo-horizontal.svg">
+                <hr />
+                <p><b>Link Akaunting into kinbrio</b></p>
+                <p></p>
+                <div class="row justified">
+                    <div class="col-sm-12 col-md-12 col-lg-6">
+                        <form id="akaunting_form" style="text-align:left;">
+                            <label for="user_name">User Name</label><br>
+                            <input type="text" id="user_name" name="user_name" value="{{akaunting_options.user_name}}">
+
+                            <label for="user_pass">User Pass</label><br>
+                            <input type="password" id="user_pass" name="user_pass"
+                                value="{{akaunting_options.user_pass}}">
+
+                            <label for="akaunting_domain">Akaunting Domain</label><br>
+                            <input type="text" id="akaunting_domain" name="akaunting_domain"
+                                value="{{akaunting_options.akaunting_domain}}">
+
+                            <label for="akaunting_company_id">Akaunting Company ID</label><br>
+                            <input type="text" id="akaunting_company_id" name="akaunting_company_id"
+                                value="{{akaunting_options.akaunting_company_id}}">
+
+                            <input type="checkbox" id="organization_data" name="organization_data"
+                                value="{{akaunting_options.organization_data}}" {% if
+                                akaunting_options.organization_data %} checked{% endif %}>
+                            <label for="organization_data">Company Data</label><br>
+
+                            <input type="checkbox" id="employee_data" name="employee_data"
+                                value="{{akaunting_options.employee_data}}" {% if akaunting_options.employee_data %}
+                                checked{% endif %}>
+                            <label for="employee_data">Users/Employees</label><br>
+
+                            <input type="checkbox" id="client_data" name="client_data"
+                                value="{{akaunting_options.client_data}}" {% if akaunting_options.client_data %}
+                                checked{% endif %}>
+                            <label for="client_data">Clients</label><br>
+
+                            <input type="checkbox" id="vendor_data" name="vendor_data"
+                                value="{{akaunting_options.vendor_data}}" {% if akaunting_options.vendor_data %}
+                                checked{% endif %}>
+                            <label for="vendor_data">Vendors</label><br>
+
+                            <input type="checkbox" id="item_data" name="item_data"
+                                value="{{akaunting_options.item_data}}" {% if akaunting_options.item_data %} checked{%
+                                endif %}>
+                            <label for="item_data">Services & Items</label><br>
+
+                            <input type="checkbox" id="invoice_data" name="invoice_data"
+                                value="{{akaunting_options.invoice_data}}" {% if akaunting_options.invoice_data %}
+                                checked{% endif %}>
+                            <label for="invoice_data">Invoices & Payments</label><br>
+
+                            <input type="checkbox" id="allow_post" name="allow_post"
+                                value="{{akaunting_options.allow_post}}" {% if akaunting_options.allow_post %} checked{%
+                                endif %}>
+                            <label for="allow_post">Creation of Invoices & Payments</label><br>
+                            <div>
+                                <input class="add_button" type="submit" value="Save" />
+                            </div>
+                        </form>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-6">
+                        <div class="content">
+                            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                                <p><b>Companys</b></p>
+                                {% for company in companys %}
+                                <div class="white-backed bump">
+                                    {{ company.name }} ({{ company.id }})
+                                    {% if company.id.to_string() == akaunting_options.akaunting_company_id %}<p><b>Selected Company</b></p>{%else%}<button onclick="set_company_id('{{ company.id }}')" class="add_button"
+                                        type="button">Select</button>{%endif%}
+                                </div>
+                                {% endfor %}
+                            </div>
+
+                            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                                <p><b>Items</b></p>
+                                {% for item in items %}
+                                <div class="white-backed bump">
+                                    {{ Self::get_str(self, item.name) }}
+                                    <form class="list_form" id="akaunting_form" action="/akaunting/import_item" method="post" enctype="multipart/form-data">
+                                        <input style="display:none" type="text" id="import_id" name="import_id"
+                                            value="{{item.id}}">
+                                        <input class="add_button" type="submit" value="{% if item.kinbrio_id.is_some() %}Reimport{%else%}Import{%endif%}" /> 
+                                    </form>
+                                </div>
+                                {% endfor %}
+                            </div>
+
+                            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                                <p><b>Users</b></p>
+                                {% for user in users %}
+                                <div class="white-backed bump">
+                                    {{ user.name }}
+                                </div>
+                                {% endfor %}
+                            </div>
+
+                            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                                <p><b>Customers</b></p>
+                                {% for customer in customers %} 
+                                <div class="white-backed bump">
+                                    {{  customer.name }}
+                                    <form class="list_form" id="akaunting_form" action="/akaunting/import_customer" method="post" enctype="multipart/form-data">
+                                        <input style="display:none" type="text" id="import_id" name="import_id"
+                                            value="{{customer.id}}">
+                                        <input class="add_button" type="submit" value="{% if customer.kinbrio_id.is_some() %}Reimport{%else%}Import{%endif%}" /> 
+                                    </form>
+                                </div>
+                                {% endfor %}
+                            </div>
+                            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                                <p><b>Invoices</b></p>
+                                {% for invoice in invoices %}
+                                <div class="white-backed bump">
+                                    {{ Self::get_str(self, invoice.document_number) }} | {{ Self::get_str(self, invoice.status) }}
+                                </div>
+                                {% endfor %}
+                            </div>
+                        </div>
+                        <div class="backed col-sm-12 col-md-12 col-lg-12">
+                            <p>What the hell is <a target="_blank" href="https://akaunting.com/"><u>Akaunting</u></a>
+                                and <a target="_blank" href="/open-source-book-keeping">why do I care</a>?</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <script>
+            window.addEventListener('load', function () {
+                post_form("akaunting_form", "/akaunting", data => {
+                    const key = "{{akaunting_options.key}}"
+                    data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+                    data.owner_key = "{{organization.owner_key}}"
+                    data.organization_key = "{{organization.key}}";
+                    data.last_sync = parseInt("{{akaunting_options.last_sync}}") || 0;
+                    data.created = parseInt("{{akaunting_options.created}}") || 0;
+                    data.updated = parseInt("{{akaunting_options.updated}}") || 0;
+                    return data
+                }, (response_text) => {
+                    window.location.reload()
+                });
+            })
+        </script>
+        {% endblock %}

+ 181 - 0
templates/board.html

@@ -0,0 +1,181 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if board.name.len() > 0 %}{{board.name}}{%else%}Create Board{% endif %}{% endblock %}
+{% block description %}{{board.description}}{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="row justified">
+    <div class="center">
+      <div class="backed col-sm-12 col-md-12 col-lg-12">
+        <h1>Board {{board.name}}</h1>
+        {% if board.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+        <button id="delete" class="delete_button center">🗑️ Delete</button>
+        {% endif %}
+        <div class="tabbed">
+
+          <input type="radio" id="tab1" name="css-tabs" {% if
+            board.key.to_string()=="00000000-0000-0000-0000-000000000000" %}checked{% endif %}>
+          {% if board.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+          <input type="radio" id="tab2" name="css-tabs" checked>
+          {% endif %}
+
+          <ul class="tabs">
+            <li class="tab"><label for="tab1">Details</label></li>
+            {% if board.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+            <li class="tab"><label for="tab2">View</label></li>
+            {% endif %}
+          </ul>
+          <div class="tab-content">
+            <div class="row justified">
+              <div class="content">
+                <form id="add_board_form">
+                  <label for="name">Name</label>
+                  <input type="text" name="name" id="name" placeholder="Name" value="{{board.name}}" />
+                  <label for="description">Description</label>
+                  <textarea name="description" id="description" placeholder="Description"
+                    value="{{board.description}}">{{board.description}}</textarea>
+                  <div>
+                    <label for="columns">Columns</label>
+                    <input type="text" name="columns" id="columns" value='{{board.columns.join(",")}}' />
+                  </div>
+                  <label>Lanes</label>
+                  <select id="lanes" multiple name="lanes">
+                    {% for status in crate::task::TaskStatus::iter() %}
+                    <option value='{{status.to_string().replace(" ", "")}}' {% if Self::lane_contained(self, status,
+                      board.lanes) %}selected{%endif%}>
+                      {{status.to_string()}}</option>
+                    {% endfor %}
+                  </select>
+                  <div>
+                    <label for="filter">Filter</label>
+                    <input type="text" name="filter" id="filter" value="{{board.filter}}" />
+                  </div>
+                  <div>
+                    <input type="submit" class="add_button"
+                      value="{% if board.name.len() == 0 %}Create board!{%else%}Update board{% endif %}" />
+                  </div>
+                </form>
+              </div>
+            </div>
+          </div>
+
+          {% if board.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+          <div class="tab-content">
+            <div class="container">
+              <div class="row justified">
+                {% for lane in board.lanes %}
+                <div class="backed col-sm-12 col-md-4 col-lg-3">
+                  <h2>{{Self::lane_name(self, lane)}}</h2>
+                  <div class="row container">
+                    {% for task in Self::get_tasks(self, lane.to_string(), tasks) %}
+                    <div class="col-sm-12 col-md-12 col-lg-6">
+                      <form id="add_task_form_{{task.key}}">
+                        <a href="/task/{{task.key}}"><b>{{task.name}}</b></a>
+                        <hr />
+                        {{task.description}}
+                        <hr />
+                        <div class="row container">
+                          <div class="col-sm-12 col-md-12 col-lg-6">
+                            <label>Assign</label>
+                            <select name="assignee_key">
+                              <option value='{{uuid::Uuid::nil()}}' {% if task.assignee_key==uuid::Uuid::nil()
+                                %}selected{% endif %}>Unassigned</option>
+                              {% for u in users %}
+                              <option value='{{u.key}}' {% if task.assignee_key==u.key %}selected{% endif %}>
+                                {{u.to_string()}}
+                              </option>
+                              {% endfor %}
+                            </select>
+                          </div>
+                          <div class="col-sm-12 col-md-12 col-lg-6">
+                            <label>Status</label>
+                            <select name="status">
+                              {% for status in TaskStatus::iter() %}
+                              <option value='{{status.to_string().replace(" ", "")}}' {% if status==task.status %}selected{% endif %}>
+                                {{status.to_string()}}
+                              </option>
+                              {% endfor %}
+                            </select>
+                          </div>
+                        </div>
+                        <input type="submit" class="add_button" value="Update Task" />
+                      </form>
+                    </div>
+                    {% endfor %}
+                  </div>
+                </div>
+                {% endfor %}
+              </div>
+            </div>
+          </div>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+
+    <script>
+      window.addEventListener('load', function () {
+        {% if board.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+        send_delete("delete", "/board/{{board.key}}", (deleted, res) => {
+          if (deleted) {
+            window.location.href = `/`
+          }
+        })
+        {% endif %}
+        post_form("add_board_form", "/board", data => {
+          const key = "{{board.key}}"
+          data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+          data.organization_key = "{{user.organization_key}}"
+          data.owner_key = "{{user.key}}"
+          data.name = data.name || "";
+          data.description = data.description || "";
+          data.columns = (data.columns || []).split(",");
+          const selected_lanes = document.getElementById("lanes").selectedOptions;
+          data.lanes = [];
+          for (var i = 0; i < selected_lanes.length; i++) {
+            data.lanes.push(selected_lanes[i].value)
+          }
+          data.filter = data.filter || "";
+          data.created = data.created || 0;
+          data.updated = data.updated || 0;
+          return data;
+        }, (response_text) => {
+          const object = JSON.parse(response_text);
+          window.location.href = `/board/${object.key}`
+        });
+
+        {% for lane in board.lanes %}
+        {% for task in Self:: get_tasks(self, lane.to_string(), tasks) %}
+        post_form("add_task_form_{{task.key}}", "/task", data => {
+          const key = "{{task.key}}"
+          data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+          data.organization_key = "{{user.organization_key}}"
+          data.owner_key = "{{user.key}}"
+          data.project_key = "{{task.project_key}}"
+          data.estimated_quarter_days = num_from_string("{{task.estimated_quarter_days}}")
+          data.due = parseInt("{{task.due}}");
+          data.start = parseInt("{{task.start}}");
+          data.name = "{{ task.due }}";
+          data.description = "{{ task.description }}";
+          data.tags = "{{ task.tags }}";
+          data.status = data.status;
+          data.assignee_key = data.assignee_key;
+          data.created = parseInt("{{task.created}}");
+          data.updated = Math.floor(new Date().getTime() / 1000);
+          return data;
+        }, (response_text) => {
+          window.location.href = `/board/{{board.key}}`
+        });
+        {% endfor %}
+        {% endfor %}
+      })
+
+
+    </script>
+    {% endblock %}

+ 0 - 0
templates/client_site/about.html


+ 0 - 0
templates/client_site/contact.html


+ 53 - 0
templates/client_site/home.html

@@ -0,0 +1,53 @@
+{% extends "layout.html" %}
+
+{% block title %}Home{% endblock %}
+{% block description %}{{description}}{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div style="text-align: center">
+    <img  decoding="async" loading="lazy" src="/fs/images/logo_transparent.png" width="320px" class="logo" alt="Kinbrio: working better together">
+</div>
+<div
+    style="background-color:rgba(175, 255, 255, 0.5); text-align: center; padding: 0 3% 0 3%; border-top: 2px solid #ffff008f;border-bottom: 2px solid #ffff008f;">
+    <h1 style="text-decoration: underline; font-weight: bold;">{{header}}</h1>
+    <h2>{{subheader}}</h2>
+    <h3>{{description}}</h3>
+</div>
+
+<div class="row" style="justify-content: center; text-align: center;">
+    <a class="button" href="/login">Login</a>
+    <a class="button" href="/register">Sign Up!</a>
+</div>
+<div class="container">
+    <div class="row" style="justify-content: center;">
+        <div class="card flex-center col-sm-12 col-md-12 col-lg-12"
+            style="justify-content: center; text-align: center; max-width: 90%;">
+            <p><b>Efficient Communication at Your Fingertips</b></p>
+            <hr />
+            <p>Communication is the lifeblood of any organization, and at Kinbrio, we take it seriously. Our platform
+                offers
+                real-time communication with voice, video, and screen sharing capabilities through the flexible Matrix
+                protocol.
+                Say goodbye to being tied to a single provider – with Matrix, you have the freedom to choose the web and
+                desktop
+                clients that best suit your preferences.</p>
+        </div>
+        <div class="row" style="justify-content: center;">
+            {% for blk in blocks %}
+            <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+                <div class="card">
+                    <p><img  decoding="async" loading="lazy" src="{{blk.img}}" width=64 height=64/></p>
+                    <p><b>{{blk.title}}</b></p>
+                    <hr />
+                    <p>{{blk.content}}</p>
+                </div>
+            </div>
+            {% endif %}
+        </div>
+    </div>
+    {% endblock %}

+ 55 - 0
templates/client_site/layout.html

@@ -0,0 +1,55 @@
+<!doctype html>
+
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <title>{% block title %} {{ title }} {% endblock %}</title>
+  <meta name="description" content="{% block description %}{% endblock %}">
+  <meta name="author" content="{% block title %} {{ author }} {% endblock %}">
+
+  <meta property="og:title" content="{% block title %} {{ title }} {% endblock %}">
+  <meta property="og:type" content="website">
+  <meta property="og:url" content="{% block canonical %}{% endblock %}">
+  <meta property="og:description" content="{% block description %}{% endblock %}">
+  <meta property="og:image" content="{% block image %}{% endblock %}">
+
+  <link rel="icon" href="/fs/favicon.ico">
+  <link rel="apple-touch-icon" href="/fs/apple-touch-icon.png">
+
+  {% block head %}{% endblock %}
+</head>
+
+<body>
+  <nav id="menu">
+    <div class="row" style="justify-content: center;">
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Landing page" href="/">🏠 Home</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="About Us" href="/">About</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Contact" href="/">Contact Us</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Services" href="/">Services</a>
+      </div>
+    
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Come chat with us directly over matrix about any questions and issues." target="_blank" href="https://matrix.to/#/#SanturceSoftware:matrix.org">Come chat with us directly over matrix about any questions and issues</a>
+      </div>
+      
+    </div>
+  </nav>
+  <div id="content">
+    {% block content %}{% endblock %}
+  </div>
+  <script async src="/fs/js/app.js"></script>
+  <link rel="stylesheet" defer href="/fs/css/mini-default.min.css">
+  <link rel="stylesheet" defer href="/fs/css/styles.css">
+</body>
+
+</html>

+ 0 - 0
templates/client_site/services.html


+ 233 - 0
templates/contact.html

@@ -0,0 +1,233 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if contact.first_name.len() > 0 %}{{contact.first_name}} {{contact.last_name}}{%else%}Create
+Contact{% endif %}{% endblock %}
+{% block description %}{{contact.description}}{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="row justified">
+    <div class="backed col-sm-12 col-md-12 col-lg-12">
+      <h1>{{contact.first_name}} {{contact.last_name}}</h1>
+      {% if contact.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+      <a class="button center" href="/note/add/Contact/{{contact.key}}">➕ Add Note</a>
+      <a class="button center" href="/file/add/Contact/{{contact.key}}">➕ Attach File</a>
+      <button id="delete" class="delete_button center">🗑️ Delete</button>
+      {% endif %}
+      <div class="row justified">
+        <div class="content">
+          <div class="tabbed">
+            <input type="radio" id="tab1" name="css-tabs" checked>
+            <input type="radio" id="tab3" name="css-tabs">
+            <ul class="tabs">
+              <li class="tab"><label for="tab1">Details</label></li>
+              <li class="tab"><label for="tab3">Documents</label></li>
+            </ul>
+            <div class="tab-content">
+              <form id="add_contact_form">
+                <div>
+                  <div class="row">
+                    <div class="col-sm-12 col-md-8 col-lg-5">
+                      <label for="first_name">First</label>
+                      <input type="text" name="first_name" id="first_name" placeholder="First Name"
+                        value="{{contact.first_name}}" />
+                    </div>
+                    <div class="col-sm-12 col-md-4 col-lg-2">
+                      <label for="middle_initial">Initital</label>
+                      <input type="text" name="middle_initial" id="middle_initial" placeholder="Initial"
+                        value="{{contact.middle_initial}}" />
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-5">
+                      <label for="last_name">Last</label>
+                      <input type="text" name="last_name" id="last_name" placeholder="Last Name"
+                        value="{{contact.last_name}}" />
+                    </div>
+                  </div>
+                </div>
+                <div>
+                  <label for="description">Description</label>
+                  <textarea name="description" id="description" placeholder="Description"
+                    value="{{contact.description}}">{{contact.description}}</textarea>
+                </div>
+                <div>
+                  <label for="position">Position</label>
+                  <input type="text" name="position" id="position" placeholder="Position"
+                    value="{{contact.position}}" />
+                </div>
+                <div>
+                  <label for="email">Email</label>
+                  <input type="text" name="email" id="email" placeholder="Email" value="{{contact.email}}" />
+                </div>
+                <div>
+                  <label for="secondary_email">Secondary Email</label>
+                  <input type="text" name="secondary_email" id="secondary_email" placeholder="Email"
+                    value="{{contact.secondary_email}}" />
+                </div>
+                <div>
+                  <label for="phone">Phone</label>
+                  <input type="text" name="phone" id="phone" placeholder="Phone" value="{{contact.phone}}" />
+                </div>
+                <div>
+                  <label for="secondary_phone">Secondary Phone</label>
+                  <input type="text" name="secondary_phone" id="secondary_phone" placeholder="Phone"
+                    value="{{contact.secondary_phone}}" />
+                </div>
+                <div>
+                  <label for="matrix_user_id">Matrix User ID</label>
+                  <input type="text" name="matrix_user_id" id="matrix_user_id" placeholder="matrix_user_id"
+                    value="{{contact.matrix_user_id}}" />
+                </div>
+                <div>
+                  <label for="web_url">Web URL</label>
+                  <input type="text" name="web_url" id="web_url" placeholder="Web URL" value="{{contact.web_url}}" />
+                </div>
+                <div>
+                  <label for="avatar_url">avatar_url</label>
+                  <input type="text" name="avatar_url" id="avatar_url" placeholder="avatar_url"
+                    value="{{contact.avatar_url}}" />
+                </div>
+                <div>
+                  <label for="address_primary">Address</label>
+                  <input type="text" name="address_primary" id="address_primary" placeholder="Address"
+                    value="{{contact.address_primary}}" />
+                </div>
+                <div>
+                  <label for="address_unit">Unit</label>
+                  <input type="text" name="address_unit" id="address_unit" placeholder="Unit"
+                    value="{{contact.address_unit}}" />
+                </div>
+                <div>
+                  <label for="city">City</label>
+                  <input type="text" name="city" id="city" placeholder="City" value="{{contact.city}}" />
+                </div>
+                <div>
+                  <label for="state">State</label>
+                  <input type="text" name="state" id="state" placeholder="State" value="{{contact.state}}" />
+                </div>
+                <div>
+                  <label for="zip_code">Zip</label>
+                  <input type="text" name="zip_code" id="zip_code" placeholder="Zipcode" value="{{contact.zip_code}}" />
+                </div>
+                <div>
+                  <label for="country">Country</label>
+                  <input type="text" name="country" id="country" placeholder="Country" value="{{contact.country}}" />
+                </div>
+                <div>
+                  <input type="submit" class="add_button"
+                    value="{% if contact.first_name.len() == 0%}Create contact!{%else%}Update contact{% endif %}" />
+                </div>
+              </form>
+            </div>
+
+            <div class="tab-content">
+              <div class="row justified ">
+                <div class="col-sm-12 col-md-6 col-lg-6">
+                  {% if notes.len() > 0 %}
+                  <h3>Notes & Docs</h3>
+                  {% for note in notes %}
+                  <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+                    <a href="/note/{{note.key}}">
+                      <p><b>{{note.title}}</b></p>
+                    </a>
+                  </div>
+                  {% endfor %}
+                  {%else%}
+                  <div class="white-backed">
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <h3>Add Your First Note</h3>
+                      <h4>Create a note at the project level, project onboarding, project style guides and any project
+                        notes that you want organized under this project</h4>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <a class="button" href="/note/add/Contact/{{contact.key}}">Add Note</a>
+                    </div>
+                  </div>
+                  {% endif %}
+                </div>
+                <div class="col-sm-12 col-md-6 col-lg-6">
+                  {% if files.len() > 0 %}
+                  <h3>Files</h3>
+                  {% for file in files %}
+                  <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+                    <a href="/file/{{file.key}}">
+                      <p><b>{{file.name}}</b></p>
+                      <hr />
+                      <p>{{file.description}}</p>
+                    </a>
+                  </div>
+                  {% endfor %}
+                  {%else%}
+                  <div class="white-backed">
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <h3>Attach Your First File</h3>
+                      <h4>Attach a file to be visible for the project. Easily share and access files you store here.
+                      </h4>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <a class="button" href="/file/add/Contact/{{contact.key}}">Add File</a>
+                    </div>
+                  </div>
+                  {% endif %}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    window.addEventListener('load', function () {
+      {% if contact.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+      send_delete("delete", "/contact/{{contact.key}}", (deleted, res) => {
+        if (deleted) {
+          window.location.href = `/entity/{{contact.entity_key}}`
+        }
+      })
+      {% endif %}
+      post_form("add_contact_form", "/contact", data => {
+        const key = "{{contact.key}}"
+        data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+        data.organization_key = "{{user.organization_key}}"
+        data.external_accounting_id = data.external_accounting_id || "{{contact.external_accounting_id}}"
+        data.owner_key = "{{user.key}}"
+        data.entity_key = "{{contact.entity_key}}"
+        data.first_name = data.first_name || "";
+        data.middle_initial = data.middle_initial || "";
+        data.last_name = data.last_name || "";
+        data.description = data.description || "";
+        data.position = data.position || "";
+        data.email = data.email || "";
+        data.phone = data.phone || "";
+        data.secondary_email = data.secondary_email || "";
+        data.secondary_phone = data.secondary_phone || "";
+
+        data.matrix_user_id = data.matrix_user_id || "";
+        data.web_url = data.web_url || "";
+        data.avatar_url = data.avatar_url || "";
+        data.social_urls = data.social_urls || [];
+        data.address_primary = data.address_primary || "";
+        data.address_unit = data.address_unit || "";
+        data.city = data.city || "";
+        data.state = data.state || "";
+        data.zip_code = data.zip_code || "";
+        data.country = data.country || "";
+        data.created = data.created || 0;
+        data.updated = data.updated || 0;
+
+        data.created = data.created || 0;
+        data.updated = data.updated || 0;
+        return data;
+      }, (response_text) => {
+        const object = JSON.parse(response_text);
+        window.location.href = `/contact/${object.key}`
+      });
+    })
+  </script>
+  {% endblock %}

+ 234 - 0
templates/dashboard.html

@@ -0,0 +1,234 @@
+{% extends "layout.html" %}
+
+{% block title %}Home{% endblock %}
+{% block description %}User Dashboard{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="backed col-sm-12 col-md-12 col-lg-12">
+  <p>
+  <h3>{{user.email}} <a href="/organization/{{organization.key}}">{{organization.name}}</a></h3>
+  </p>
+
+  <a class="button" href="/project/add">➕ add project</a>
+  <a class="button" href="/board/add">➕ add board</a>
+  <a class="button" href="/service_item/add">➕ add products & services</a>
+  <a class="button" href="/entity/add">➕ add entity</a>
+  <a class="button center" href="/note/add/Organization/{{organization.key}}">➕ Add Note</a>
+  <a class="button center" href="/file/add/Organization/{{organization.key}}">➕ Attach File</a>
+  <div class="tabbed">
+    <input type="radio" id="tab1" name="css-tabs" checked>
+    <input type="radio" id="tab2" name="css-tabs">
+    <input type="radio" id="tab3" name="css-tabs">
+    <input type="radio" id="tab4" name="css-tabs">
+    <input type="radio" id="tab5" name="css-tabs">
+
+    <ul class="tabs">
+      <li class="tab"><label for="tab1">Projects</label></li>
+      <li class="tab"><label for="tab2">Boards</label></li>
+      <li class="tab"><label for="tab3">Services & Products</label></li>
+      <li class="tab"><label for="tab4">Contacts & Entitys</label></li>
+      <li class="tab"><label for="tab5">Documents</label></li>
+    </ul>
+
+    <div class="tab-content">
+      {% if projects.len() > 0 %}
+      <h3>Your Projects</h3>
+      <div class="row">
+        {% for project in projects %}
+        <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+          <a href="/project/{{project.key}}">
+            <p><b>{{project.name}}</b></p>
+            <hr />
+            <p>{{project.description}}</p>
+          </a>
+        </div>
+        {% endfor %}
+      </div>
+      {% else %}
+      <div class="row white-backed ">
+        <div class="col-sm-12 col-md-12 col-lg-12">
+          <h3>Create Your First Project</h3>
+          <h4>Keep track of the different projects you have across your marketing, sales, R&D, business development, and
+            executive teams.</h4>
+          <h5>Create tasks, schedules, and milestones. Keep organized notes, documentation and files relevant to the
+            project in one easily accessible place.</h5>
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          <a class="button" href="/project/add">➕ add project</a>
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          <a class="button" style="text-decoration: underline;" href="/akaunting">Import From Akaunting</a>
+        </div>
+      </div>
+      {% endif %}
+    </div>
+    <div class="tab-content">
+      {% if user_boards.len() > 0 %}
+      <h3>Your Boards</h3>
+      <div class="row">
+        {% for board in user_boards %}
+        <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+          <a href="/board/{{board.key}}">
+            <p><b>{{board.name}}</b></p>
+            <hr />
+            <p>{{board.description}}</p>
+          </a>
+        </div>
+        {% endfor %}
+      </div>
+      {% else %}
+      <div class="row white-backed ">
+        <div class="col-sm-12 col-md-12 col-lg-12">
+          <h3>Create Your First Board</h3>
+          <h4>Track and filter tasks with boards to help you manage tasks across lots of projects and teams.</h4>
+          <h5>Customize columns and filters to view what's happening exactly how you need</h5>
+        </div>
+        <div class="col-sm-12 col-md-12 col-lg-12">
+          <a class="button" href="/board/add">➕ add board</a>
+        </div>
+      </div>
+      {% endif %}
+    </div>
+
+    <div class="tab-content">
+      {% if service_items.len() > 0 %}
+      <h3>Services and Products</h3>
+      <div class="row">
+        {% for service_item in service_items %}
+        <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+          <a href="/service_item/{{service_item.key}}">
+            <p><b>{{service_item.name}}</b></p>
+            <hr />
+            <p>{{service_item.description}}</p>
+          </a>
+        </div>
+        {% endfor %}
+      </div>
+      {% else %}
+      <div class="row white-backed">
+        <div class="col-sm-12 col-md-12 col-lg-12">
+          <h3>Import your items and services from akaunting or add some</h3>
+          <h4>Link your projects and tasks to items and services to help organize and visualize your operations even
+            better.</h4>
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          <a class="button" href="/service_item/add">Add some products/services</a>
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          <a class="button" style="text-decoration: underline;" href="/akaunting">Import From Akaunting</a>
+        </div>
+      </div>
+      {% endif %}
+    </div>
+
+    <div class="tab-content">
+      <div class="row justified ">
+        <div class="col-sm-12 {% if contacts.len() > 0 %}col-md-6 col-lg-6}{%else%}col-md-12 col-lg-12{%endif%}">
+          {% if entitys.len() > 0 %}
+          <h3>Entitys</h3>
+          <div class="row">
+            {% for entity in entitys %}
+            <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+              <a href="/entity/{{entity.key}}">
+                <p><b>{{entity.name}}</b></p>
+                <hr />
+              </a>
+            </div>
+            {% endfor %}
+          </div>
+          {%else%}
+          <div class="row white-backed">
+            <div class="col-sm-12 col-md-12 col-lg-12">
+              <h3>Import your customers and suppliers from akaunting or add some</h3>
+              <h4>Link different entities to tasks, projects, items and services to help organize and automate your
+                operations to the fullest.</h4>
+            </div>
+            <div class="col-sm-12 col-md-6 col-lg-6">
+              <a class="button" href="/entity/add">Add New Entity</a>
+            </div>
+            <div class="col-sm-12 col-md-6 col-lg-6">
+              <a class="button" style="text-decoration: underline;" href="/akaunting">Import From Akaunting</a>
+            </div>
+          </div>
+          {% endif %}
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          {% if contacts.len() > 0 %}
+          <h3>Global Rolodex</h3>
+          <div class="row">
+            {% for contact in contacts %}
+            <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+              <a href="/contact/{{contact.key}}">
+                <p><b>{{contact.first_name}} {{contact.last_name}}</b></p>
+                <hr />
+              </a>
+            </div>
+            {% endfor %}
+          </div>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+    <div class="tab-content">
+      <div class="row justified ">
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          {% if notes.len() > 0 %}
+          <h3>Notes & Docs</h3>
+          <div class="row">
+            {% for note in notes %}
+            <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+              <a href="/note/{{note.key}}">
+                <p><b>{{note.title}}</b></p>
+                <hr />
+              </a>
+            </div>
+            {% endfor %}
+          </div>
+          {%else%}
+          <div class="row white-backed">
+            <div class="col-sm-12 col-md-12 col-lg-12">
+              <h3>Add Your First Note</h3>
+              <h4>Create a note at the Organization level: internal documentation, onboarding, style guides and employee
+                guide lines can be created and accessible by all teams across all projects</h4>
+            </div>
+            <div class="col-sm-12 col-md-12 col-lg-12">
+              <a class="button center" href="/note/add/Organization/{{organization.key}}">➕ Add Note</a>
+            </div>
+          </div>
+          {% endif %}
+        </div>
+        <div class="col-sm-12 col-md-6 col-lg-6">
+          {% if files.len() > 0 %}
+          <h3>Files</h3>
+          <div class="row">
+            {% for file in files %}
+            <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+              <a href="/file/{{file.key}}">
+                <p><b>🖇️ {{file.name}} {{file.description}}</b></p>
+                <hr />
+              </a>
+            </div>
+            {% endfor %}
+          </div>
+          {%else%}
+          <div class="row white-backed">
+            <div class="col-sm-12 col-md-12 col-lg-12">
+              <h3>Attach Your First File</h3>
+              <h4>Attach a file to be visible for the organization. Easily share and access files you store here.</h4>
+            </div>
+            <div class="col-sm-12 col-md-12 col-lg-12">
+              <a class="button center" href="/file/add/Organization/{{organization.key}}">➕ Attach File</a>
+            </div>
+          </div>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}

+ 130 - 0
templates/documentation.html

@@ -0,0 +1,130 @@
+{% extends "layout.html" %}
+
+{% block title %}Documentation{% endblock %}
+{% block description %}Complete documentation for maximizing your Kinbrio experience.{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div style="text-align: center">
+  <img  decoding="async" loading="lazy" src="/fs/images/logo_transparent.png" width="320px" class="logo" alt="Kinbrio: working better together">
+</div>    
+<div style="background-color:rgba(175, 255, 255, 0.5); text-align: center; padding: 0 3% 0 3%; border-top: 2px solid #ffff008f;border-bottom: 2px solid #ffff008f;">
+  <h1 style="text-decoration: underline; font-weight: bold;">Let's Get Started!</h1>
+  <h2>From initial login to operating on all cylinders in no time!</h2>
+</div>
+<div class="container">
+  <div class="row" style="justify-content: center;">
+    <div class="card flex-center col-sm-12 col-md-12 col-lg-12"
+      style="justify-content: center; text-align: center; max-width: 90%;">
+      <p><b>Efficient Communication at Your Fingertips</b></p>
+      <hr />
+      <p>Communication is the lifeblood of any organization, and at Kinbrio, we take it seriously. Our platform offers
+        real-time communication with voice, video, and screen sharing capabilities through the flexible Matrix
+        protocol.
+        Say goodbye to being tied to a single provider – with Matrix, you have the freedom to choose the web and
+        desktop
+        clients that best suit your preferences.</p>
+    </div>
+  </div>
+  <div class="row" style="justify-content: center;">
+    <div class="col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Effortless Organization Management</b></p>
+        <hr />
+        <p>Manage your entire organization effortlessly with Kinbrio. From creating teams and communication channels to
+          handling internal documentation and deploying branding and digital web presence, our intuitive dashboard puts
+          you
+          in control.</p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Seamless Project Management</b></p>
+        <hr />
+        <p>No more juggling endless tasks and milestones. With Kinbrio, project management becomes a breeze. Schedule,
+          track
+          status, and integrate with internal communications to keep everyone on the same page, always.</p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Empowering Accounting Management</b></p>
+        <hr />
+        <p>Keep your finances in check with ease. Kinbrio allows you to handle simple invoices and sales effortlessly.
+          For
+          more involved accounting needs, we integrate with Akaunting or let you host your own Instance using their open
+          and
+          freely available software.</p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Client, Customer, and Supplier Management Made Simple</b></p>
+        <hr />
+        <p>Effortlessly manage client and customer data, documentation, and notes with historical context. Kinbrio also
+          enables secure and encrypted communication with clients through dedicated Matrix rooms. Meanwhile, supplier
+          management becomes a breeze with seamless tracking, meeting, and reporting capabilities.</p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Efficient Inventory Management</b></p>
+        <hr />
+        <p>Stay on top of your products with our comprehensive inventory management system. From cost and pricing to
+          quantities and documentation, Kinbrio helps you optimize your inventory and streamline sales reporting.</p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>AI-Powered Insights for Smart Decision Making</b></p>
+        <hr />
+        <p>Harness the power of AI to answer your organizational questions quickly. Our AI powered assistants help you find
+          historical chats and provide assistance with answering simple questions, saving you valuable time and effort.
+        </p>
+      </div>
+    </div>
+
+    <div class=" col-sm-12 col-md-6 col-lg-3 flex-center" style="justify-content: center; text-align: center">
+      <div class="card">
+        <p><b>Creating Your Digital Presence with Kinbrio</b></p>
+        <hr />
+        <p>Turn your ideas into reality with Kinbrio's full suite of services. From generating landing pages and product
+          pages to custom web applications and technical consulting, we make sure your business operates at its full
+          potential.</p>
+      </div>
+    </div>
+
+    <div class="card col-sm-12 col-md-12 col-lg-12"
+      style="justify-content: center; text-align: center; max-width: 90%;justify-content: center;">
+      <p><b>Join Kinbrio Today!</b></p>
+      <hr />
+      <p>Experience the power of Kinbrio in revolutionizing your business. Contact us now to explore how our innovative
+        software platform can take your organization to new heights. Together, let's build a future of success and
+        growth with Kinbrio!</p>
+    </div>
+  </div> 
+</div>
+<div class="row" style="justify-content: center; text-align: center;">
+  <a class="button" href="/login">Login</a>
+  <a class="button" href="/register">Sign Up!</a>
+</div>
+<div style="background-color:rgba(175, 255, 255, 0.5); text-align: center; padding: 0 3% 0 3%">
+  <h4>It's YOUR data, your business, and your communications. Exportable data to make sure you aren't having to fight to
+    access your
+    own data. Your data isn't being resold to build AI products for competitors. All communication ran off secure matrix
+    servers, a portable and extendible protocol that doesn't lock you in to any clients or even our own platform,
+    if you don't like us - find someone else and never lose any access to your chat and communications.
+    For those with special needs and security requirements, privately run your own instance and secure matrix server on
+    a local network!
+  </h4>
+</div>
+{% endblock %}

+ 203 - 0
templates/entity.html

@@ -0,0 +1,203 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if entity.name.len() > 0 %}{{entity.name}}{%else%}Create Entity{% endif %}{% endblock %}
+{% block description %}{{entity.description}}{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="row justified">
+    <div class="backed col-sm-12 col-md-12 col-lg-12">
+      <h1>Entity {{entity.name}}</h1>
+      <div class="row justified">
+        <div class="content">
+          {% if entity.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+          <a class="button" href="/contact/add/{{entity.key}}">➕ Add Contact</a>
+          <a class="button center" href="/note/add/Entity/{{entity.key}}">➕ Add Note</a>
+          <a class="button center" href="/file/add/Entity/{{entity.key}}">➕ Attach File</a>
+          <button id="delete" class="delete_button center">🗑️ Delete</button>
+          {% endif %}
+          {% if entity.external_accounting_id != "" %}
+          <a class="button" href="/entity/invoices/{{entity.key}}/{{entity.external_accounting_id}}">View Invoices</a>
+          {%endif%}
+          <div class="tabbed">
+            <input type="radio" id="tab1" name="css-tabs" checked>
+            <input type="radio" id="tab2" name="css-tabs">
+            <input type="radio" id="tab3" name="css-tabs">
+            <ul class="tabs">
+              <li class="tab"><label for="tab1">Details</label></li>
+              <li class="tab"><label for="tab2">Contacts</label></li>
+              <li class="tab"><label for="tab3">Documents</label></li>
+            </ul>
+            <div class="tab-content">
+              <form id="add_entity_form">
+                <div>
+                  <label for="name">Name</label>
+                  <input type="text" name="name" id="name" placeholder="Name" value="{{entity.name}}" />
+                </div>
+                <div>
+                  <label for="description">Description</label>
+                  <textarea name="description" id="description" placeholder="Description"
+                    value="{{entity.description}}">{{entity.description}}</textarea>
+                </div>
+
+                <div>
+                  <label for="name">Web URL (https:://acme.com)</label>
+                  <input type="text" name="web_url" id="web_url" placeholder="Web URL" value="{{entity.web_url}}" />
+                </div>
+                <div>
+                  <label for="matrix_room_url">Matrix Server Room URL (https://matrix.to/#/#SanturceSoftware:matrix.org)
+                  </label>
+                  <input type="text" name="matrix_room_url" id="matrix_room_url" placeholder="matrix_room_url"
+                    value="{{entity.matrix_room_url}}" />
+                </div>
+                <div>
+                  <label for="web_url">Web URL</label>
+                  <input type="text" name="web_url" id="web_url" placeholder="Web URL" value="{{entity.web_url}}" />
+                </div>
+                <div>
+                  <label for="avatar_url">avatar_url</label>
+                  <input type="text" name="avatar_url" id="avatar_url" placeholder="avatar_url"
+                    value="{{entity.avatar_url}}" />
+                </div>
+                <div>
+                  <label for="address_primary">Address</label>
+                  <input type="text" name="address_primary" id="address_primary" placeholder="Address"
+                    value="{{entity.address_primary}}" />
+                </div>
+                <div>
+                  <label for="address_unit">Unit</label>
+                  <input type="text" name="address_unit" id="address_unit" placeholder="Unit"
+                    value="{{entity.address_unit}}" />
+                </div>
+                <div>
+                  <label for="city">City</label>
+                  <input type="text" name="city" id="city" placeholder="City" value="{{entity.city}}" />
+                </div>
+                <div>
+                  <label for="state">State</label>
+                  <input type="text" name="state" id="state" placeholder="State" value="{{entity.state}}" />
+                </div>
+                <div>
+                  <label for="zip_code">Zip</label>
+                  <input type="text" name="zip_code" id="zip_code" placeholder="Zipcode" value="{{entity.zip_code}}" />
+                </div>
+                <div>
+                  <label for="country">Country</label>
+                  <input type="text" name="country" id="country" placeholder="Country" value="{{entity.country}}" />
+                </div>
+                <div>
+                  <input type="submit" class="add_button"
+                    value="{% if entity.name.len() == 0 %}Create entity!{%else%}Update entity{% endif %}" />
+                </div>
+              </form>
+            </div>
+
+            <div class="tab-content">
+              {% if contacts.len() > 0 %}
+              <h3>Contacts</h3>
+              <ul class="pick_list">
+                {% for contact in contacts %}
+                <li><a class="button" href="/contact/{{contact.key}}">{{contact.first_name}} {{contact.last_name}}</a>
+                </li>
+                {% endfor %}
+              </ul>
+              {% endif %}
+            </div>
+            <div class="tab-content">
+              <div class="row justified ">
+                <div class="col-sm-12 col-md-6 col-lg-6">
+                  {% if notes.len() > 0 %}
+                  <h3>Notes & Docs</h3>
+                  {% for note in notes %}
+                  <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+                    <a href="/note/{{note.key}}">
+                      <p><b>{{note.title}}</b></p>
+                    </a>
+                  </div>
+                  {% endfor %}
+                  {%else%}
+                  <div class="white-backed">
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <h3>Add Your First Note</h3>
+                      <h4>Create a note about this entity, these are private to you and allow you to keep internal notes on vendors, clients, contractors or any other entitys you keep track of</h4>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <a class="button" href="/note/add/Entity/{{entity.key}}">Add Note</a>
+                    </div>
+                  </div>
+                  {% endif %}
+                </div>
+                <div class="col-sm-12 col-md-6 col-lg-6">
+                  {% if files.len() > 0 %}
+                  <h3>Files</h3>
+                  {% for file in files %}
+                  <div class="white-backed bump col-sm-12 col-md-4 col-lg-3">
+                    <a href="/file/{{file.key}}">
+                      <p><b>{{file.name}}</b></p>
+                      <hr />
+                      <p>{{file.description}}</p>
+                    </a>
+                  </div>
+                  {% endfor %}
+                  {%else%}
+                  <div class="white-backed">
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <h3>Attach Your First File</h3>
+                      <h4>Attach a file relating to this entity. Anything from receipts to documents that you want organized with this entity.
+                      </h4>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                      <a class="button" href="/file/add/Entity/{{entity.key}}">Add File</a>
+                    </div>
+                  </div>
+                  {% endif %}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <script>
+    window.addEventListener('load', function () {
+      {% if entity.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+      send_delete("delete", "/entity/{{entity.key}}", (deleted, res) => {
+        if (deleted) {
+          window.location.href = `/`
+        }
+      })
+      {% endif %}
+      post_form("add_entity_form", "/entity", data => {
+        const key = "{{entity.key}}"
+        data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+        data.organization_key = "{{user.organization_key}}"
+        data.external_accounting_id = data.external_accounting_id || "{{entity.external_accounting_id}}"
+        data.owner_key = "{{user.key}}"
+        data.entity_type = data.entity_type || "Client";
+        data.name = data.name || "";
+        data.description = data.description || "";
+        data.matrix_room_url = data.matrix_room_url || "";
+        data.web_url = data.web_url || "";
+        data.avatar_url = data.avatar_url || "";
+        data.address_primary = data.address_primary || "";
+        data.address_unit = data.address_unit || "";
+        data.city = data.city || "";
+        data.state = data.state || "";
+        data.zip_code = data.zip_code || "";
+        data.country = data.country || "";
+        data.created = data.created || 0;
+        data.updated = data.updated || 0;
+        return data;
+      }, (response_text) => {
+        const object = JSON.parse(response_text);
+        window.location.href = `/entity/${object.key}`
+      });
+    })
+  </script>
+  {% endblock %}

+ 84 - 0
templates/file.html

@@ -0,0 +1,84 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if file.name.len() > 0 %}{{file.name}}{%else%}Add File{% endif %}{% endblock %}
+{% block description %}{{file.description}}{% endblock %}
+{% block content %}
+<div class="container">
+    <div class="row justified">
+        <div class="backed col-sm-12 col-md-12 col-lg-12">
+            <h1>{{file.name}}</h1>
+            <a href="/files/{{file.format}}/{{file.organization_key}}/{{file.association_type.to_string()}}/{{file.association_key}}/{{file.name}}">
+                <img height="128" width="128" alt="📄 View Document" src="/files/{{file.format}}/{{file.organization_key}}/{{file.association_type.to_string()}}/{{file.association_key}}/{{file.name}}"/>
+            </a>
+            {% if file.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+            <button id="delete" class="delete_button center">🗑️ Delete</button>
+            {% endif %}
+            <div class="row justified">
+                <div class="content">
+                    <form id="add_file_form" action="/file" method="post" enctype="multipart/form-data">
+                        {% if file.key.to_string() == "00000000-0000-0000-0000-000000000000" %}\
+                        <p>
+                            <label>Add file: </label><br />
+                            <input id="file_uploader" type="file" name="file" />
+                        </p>
+                        {% endif %}
+                        <div class="col-sm-12 col-md-12 col-lg-12">
+                            <label for="name">Name</label>
+                            <input type="text" name="name" id="name" placeholder="File Name" value="{{file.name}}" />
+                        </div>
+                        <div class="col-sm-12 col-md-12 col-lg-12">
+                            <label for="tags">Tags</label>
+                            <input type="text" name="tags" id="tags" placeholder="Tags" value="{{file.tags}}" />
+                        </div>
+                        <input type="text" style="display:none" name="key" id="key" value="{{file.key}}" />
+                        <input type="text" style="display:none" name="organization_key" id="organization_key" value="{{file.organization_key}}" />
+                        <input type="text" style="display:none" name="association_type" id="association_type"
+                            value="{{file.association_type.to_string()}}" />
+                        <input type="text" style="display:none" name="association_key" id="association_key"
+                            value="{{file.association_key}}" />
+                        <input type="text" style="display:none" name="url" id="url" value="{{file.url}}" />
+                        <input type="text" style="display:none" name="format" id="format" value="{{file.format}}" />
+                        <input type="text" style="display:none" name="hash" id="hash" value="{{file.hash}}" />
+                        <input type="text" style="display:none" name="size" id="size" value="{{file.size}}" />
+                        <div>
+                            <label for="description">Description</label>
+                            <textarea name="description" id="description" placeholder="Description"
+                                value="{{file.description}}">{{file.description}}</textarea>
+                        </div>
+                        <div>
+                            <input type="submit" class="add_button"
+                                value="{% if file.key.is_nil() %}Create file!{%else%}Update file{% endif %}" />
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+    window.addEventListener('load', function () {
+        document.getElementById("file_uploader").onchange = function(e) {
+            const files = event.target.files
+            if (files && files.length > 0) {
+                const filename = files[0].name
+                const extension = files[0].type
+                const splits = extension.split("/");
+                const name_input = document.getElementById("name")
+                name_input.value = filename
+                if (splits && splits.length > 0) { 
+                    const format_input = document.getElementById("format")
+                    format_input.value = splits[1]
+                }
+            }
+        };
+        {% if file.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+        send_delete("delete", "/file/{{file.key}}", (deleted, res) => {
+            if (deleted) {
+                window.location.href = `/{{file.association_type.to_string().to_lowercase()}}/{{file.association_key}}`
+            }
+        })
+        {% endif %}
+    })
+</script>
+{% endblock %}

+ 138 - 0
templates/home.html

@@ -0,0 +1,138 @@
+{% extends "layout.html" %}
+
+{% block title %}Home{% endblock %}
+{% block description %}Welcome to Kinbrio, the all-in-one platform that empowers your business with seamless
+organization, efficient communication, and powerful project and client management tools. We understand the challenges of
+running a successful business, which is why we've developed a comprehensive suite of features to help you thrive.{%
+endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<iframe style="display:none" src="https://b07e-2605-ba00-3118-1dd-d8c4-80ab-3c79-616c.ngrok-free.app/widget"></iframe>
+<div style="text-align: center">
+  <img decoding="async" src="/fs/images/logo_transparent.png" width="256px" height="192px" class="logo"
+    alt="Kinbrio: working better together">
+</div>
+<div class="backed centered">
+  <h1 style="font-weight: bold;">Your Complete Business Management Solution</h1>
+  <h2>All-in-one platform that empowers your business with seamless organization, efficient
+    communication, and powerful project and client management tools.</h2>
+  <h3>We understand the challenges of running a successful business, which is why we've developed a comprehensive suite
+    of features to help you thrive.</h3>
+</div>
+
+<div class="row justified centered">
+  <a class="button" href="/login">Login</a>
+  <a class="button" href="/register">Sign Up!</a>
+</div>
+<div class="row centered">
+  <div class="col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Efficient Communication at Your Fingertips</b></p>
+      <hr />
+      <p>Communication is the lifeblood of any organization, and at Kinbrio, we take it seriously. Our platform offers
+        real-time communication with voice, video, and screen sharing capabilities through the flexible Matrix
+        protocol.
+        Say goodbye to being tied to a single provider – with Matrix, you have the freedom to choose the web and
+        desktop
+        clients that best suit your preferences.</p>
+    </div>
+  </div>
+  <div class="col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Effortless Organization Management</b></p>
+      <hr />
+      <p>Manage your entire organization effortlessly with Kinbrio. From creating teams and communication channels to
+        handling internal documentation and deploying branding and digital web presence, our intuitive dashboard puts
+        you
+        in control.</p>
+    </div>
+  </div>
+
+  <div class=" col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Seamless Project Management</b></p>
+      <hr />
+      <p>No more juggling endless tasks and milestones. With Kinbrio, project management becomes a breeze. Schedule,
+        track
+        status, and integrate with internal communications to keep everyone on the same page, always.</p>
+    </div>
+  </div>
+
+  <div class=" col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Empowering Accounting Management</b></p>
+      <hr />
+      <p>Keep your finances in check with ease. Kinbrio allows you to handle simple invoices and sales effortlessly.
+        For
+        more involved accounting needs, we integrate with Akaunting or let you host your own Instance using their open
+        and
+        freely available software.</p>
+    </div>
+  </div>
+
+  <div class=" col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Client, Customer, and Supplier Management Made Simple</b></p>
+      <hr />
+      <p>Effortlessly manage client and customer data, documentation, and notes with historical context. Kinbrio also
+        enables secure and encrypted communication with clients through dedicated Matrix rooms. Meanwhile, supplier
+        management becomes a breeze with seamless tracking, meeting, and reporting capabilities.</p>
+    </div>
+  </div>
+
+  <div class=" col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Efficient Inventory Management</b></p>
+      <hr />
+      <p>Stay on top of your products with our comprehensive inventory management system. From cost and pricing to
+        quantities and documentation, Kinbrio helps you optimize your inventory and streamline sales reporting.</p>
+    </div>
+  </div>
+
+  <div class=" col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>AI-Powered Insights for Smart Decision Making</b></p>
+      <hr />
+      <p>Harness the power of AI to answer your organizational questions quickly. Our assistants powered with LLMs
+        fine tuned on your business can help you find
+        historical chats and provide assistance with answering simple questions, saving you valuable time and effort.
+      </p>
+    </div>
+  </div>
+
+  <div class="col-sm-12 col-md-6 col-lg-3 flex-center justified">
+    <div class="backed centered">
+      <p class="emphasized"><b>Creating Your Digital Presence with Kinbrio</b></p>
+      <hr />
+      <p>Turn your ideas into reality with Kinbrio's full suite of services. From generating landing pages and product
+        pages to custom web applications and technical consulting, we make sure your business operates at its full
+        potential.</p>
+    </div>
+  </div>
+
+  <div class="backed centered col-sm-12 col-md-12 col-lg-12 centered justified" style="max-width: 90%;">
+    <p><b>Join Kinbrio Today!</b></p>
+    <hr />
+    <p>Experience the power of Kinbrio in revolutionizing your business. Contact us now to explore how our innovative
+      software platform can take your organization to new heights. Together, let's build a future of success and
+      growth with Kinbrio!</p>
+    <a class="button" href="/login">Login</a>
+    <a class="button" href="/register">Sign Up!</a>
+  </div>
+</div>
+<div class="backed centered">
+  <h4>It's YOUR data, your business, and your communications. Exportable data to make sure you aren't having to fight to
+    access your
+    own data. Your data isn't being resold to build AI products for competitors. All communication ran off secure matrix
+    servers, a portable and extendible protocol that doesn't lock you in to any clients or even our own platform,
+    if you don't like us - find someone else and never lose any access to your chat and communications.
+    For those with special needs and security requirements, privately run your own instance and secure matrix server on
+    a local network!
+  </h4>
+</div>
+{% endblock %}

+ 33 - 0
templates/invoice_list.html

@@ -0,0 +1,33 @@
+{% extends "layout.html" %}
+
+{% block title %}Home{% endblock %}
+{% block description %}User Dashboard{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="backed col-sm-12 col-md-12 col-lg-12">
+    <p>
+    <h3>{{entity.name}}</a></h3>
+    </p> 
+    <div class="tabbed">
+            {% for invoice in invoices %}
+                <div class="row white-backed">
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                         <span style="font-size: large; font-weight: bold; color: rgba(0, 100, 0, 0.726)">${{invoice.amount}}</span>
+                          <span style="font-size: large; font-weight: bold;">{{invoice.status.as_ref().expect("status exists")}}</span>
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                         <span><b>Due Date</b> {{invoice.due_at.as_ref().expect("due exists")}}</span> 
+                    </div>
+                    <div class="col-sm-12 col-md-12 col-lg-12">
+                        <a class="button" href="https://app.akaunting.com/{{invoice.company_id}}/sales/invoices/{{invoice.id}}">View Invoice</a>
+                    </div>
+                </div>
+            {% endfor %}
+    </div>
+</div>
+{% endblock %}

+ 80 - 0
templates/layout.html

@@ -0,0 +1,80 @@
+<!doctype html>
+
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <title>{% block title %} {{ title }} {% endblock %}</title>
+  <meta name="description" content="{% block description %}{% endblock %}">
+  <meta name="author" content="Kinbrio">
+
+  <meta property="og:title" content="{% block title %} {{ title }} {% endblock %}">
+  <meta property="og:type" content="website">
+  <meta property="og:url" content="{% block canonical %}{% endblock %}">
+  <meta property="og:description" content="{% block description %}{% endblock %}">
+  <meta property="og:image" content="{% block image %}{% endblock %}">
+
+  <link rel="icon" href="/fs/favicon.ico">
+  <link rel="apple-touch-icon" href="/fs/apple-touch-icon.png">
+
+  {% block head %}{% endblock %}
+</head>
+
+<body>
+  <nav id="menu">
+    <div class="row" style="justify-content: center;">
+      {% if user.key.is_nil() -%}
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Landing page for Kinbrio" href="/">🏠 Home</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Login to Kinbrio using your matrix ID" href="/login">🔑 Login</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Register for Kinbrio using your matrix ID" href="/register">👑 Register</a>
+      </div>
+      {% else -%}
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Dashboard for projects, boards, contacts and organization wide notes and documentation" href="/dashboard">🏠 Dashboard</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Account related settings and actions" href="/account">⚙️ Account</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Organization related settings and actions" href="/organization/{{user.organization_key}}">🏢 Organization</a>
+      </div>
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <label for="assistant">🤖 AI Assistant</label>
+        <input class="checkbutton" name="ai-toggle" type="radio" id="assistant">
+        <div class="backed assistant-window">
+          <label for="minimize_assistant">🗕 Minimize</label>
+          <input class="checkbutton" name="ai-toggle" type="radio" id="minimize_assistant">
+          <iframe style="display:none;" height="800px"    width="400px" loading="lazy" src="http://localhost:3000"></iframe>
+        </div>
+      </div>
+      {% endif -%}
+      <div class="col-sm-6 col-md-3 col-lg-2 flex-center" style="justify-content: center; text-align: center">
+        <a title="Documentation and tutorials on how to operate Kinbrio to it's fullest extent" target="_blank" href="/documentation">🗎 Documentation</a>
+      </div>
+    </div>
+  </nav>
+  <div id="content">
+    {% block content %}{% endblock %}
+  </div>
+  <script async src="/fs/js/app.js"></script>
+  <link rel="stylesheet" defer href="/fs/css/mini-default.min.css">
+  <link rel="stylesheet" defer href="/fs/css/styles.css">
+  <footer>
+    <div class="row" style="justify-content: center;">
+      <div class="col-sm-12 col-md-12 col-lg-12 flex-center" style="justify-content: center; text-align: center">
+        <a title="Kinbrio Support. Come chat with us over matrix about questions and issues." target="_blank" href="https://matrix.to/#/#SanturceSoftware:matrix.org">
+          💬 Chat with us</a>
+      </div>
+
+    </div>
+    </footer>
+</body>
+
+</html>

+ 42 - 0
templates/login.html

@@ -0,0 +1,42 @@
+{% extends "layout.html" %}
+
+{% block title %}Login{% endblock %}
+{% block description %}Login to Kinbrio{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="section">
+    <div class="heading_div">
+      <h1>Welcome Back</h1>
+      <h2>Authenticate Securely Using Your <u><a href="https://matrix.org/about/">Matrix</a></u> Account</h2>
+      <h3>(Or <u><a href="/register">Register Here</a></u>)</h3>
+    </div>
+    <div class="row justified">
+      <div class="content">
+        <form class="heading_div" id="login_form">
+          {% for choice in choices %}
+            <a class="button" style="background:#ffffff" href="{{choice.url}}">
+              <img decoding="async" loading="lazy" width="32" height="32" alt="{{choice.display}} logo" src="{{ choice.logo }}">
+              <hr/>
+              <span>{{choice.display}}</span>
+            </a>
+          {% endfor %}
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  window.addEventListener('load', function () {
+    post_form("login_form", "/login", data => {
+      return data
+    }, () => { window.location.href = "/" });
+  })
+</script>
+{% endblock %}

+ 42 - 0
templates/login_username.html

@@ -0,0 +1,42 @@
+{% extends "layout.html" %}
+
+{% block title %}Login{% endblock %}
+{% block description %}Login to Kinbrio{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="section">
+    <div class="heading_div">
+      <h1>Welcome back!</h1>
+      <h2>Authenticate Securely Using Your <a href="https://matrix.org/about/">Matrix</a> Account</h2>
+      <h3>(Or Register <a href="/register">Here</a>)</h3>
+    </div>
+    <div class="row justified">
+      <div class="content">
+        <form id="login_form"  action="/login" method="post" enctype="multipart/form-data">
+          <label for="uid">Matrix User ID</label>
+          <input type="text" name="uid" id="uid" placeholder="@user:matrix.org" />
+
+          <label for="secret">Password</label>
+          <input type="password" name="secret" id="secret" placeholder="Password" />
+          <input type="submit" class="add_button" value="Login" />
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  window.addEventListener('load', function () {
+    post_form("login_form", "/login", data => {
+      data.homeserver = "https://matrix-client.matrix.org";
+      return data
+    }, () => { window.location.href = "/" });
+  })
+</script>
+{% endblock %}

+ 93 - 0
templates/milestone.html

@@ -0,0 +1,93 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if milestone.name.len() > 0 %}{{milestone.name}}{%else%}Add Milestone{% endif %}{% endblock %}
+{% block description %}{{milestone.description}}{% endblock %}
+
+{% block head %}
+<style>
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="container">
+  <div class="row justified">
+    <div class+="content">
+      <div class="backed col-sm-12 col-md-12 col-lg-12">
+        <h1>Milestone {{milestone.name}}</h1>
+        {% if milestone.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+        <a class="button center" href="/note/add/Milestone/{{milestone.key}}">➕ Add Note</a>
+        <a class="button center" href="/file/add/Milestone/{{milestone.key}}">➕ Attach File</a>
+        <button id="delete" class="delete_button center">🗑️ Delete</button>
+        {% endif %}
+        <div class="row justified">
+          <div class="content">
+            <form id="add_milestone_form">
+              <div>
+                <label for="name">Name</label>
+                <input type="text" name="name" id="name" placeholder="Name" value="{{milestone.name}}" />
+              </div>
+              <div>
+                <label for="description">Description</label>
+                <textarea name="description" id="description" placeholder="Description"
+                  value="{{milestone.description}}">{{milestone.description}}</textarea>
+              </div>
+              <div>
+                <label for="tags">Tags</label>
+                <input type="text" name="tags" id="tags" placeholder="Tags,seperated,by,commas"
+                  value="{{milestone.tags}}" />
+              </div>
+              <label for="estimated_quarter_days">Estimated Quarter Work Days</label>
+              <input type="number" id="estimated_quarter_days" name="estimated_quarter_days" min="1" max="1000"
+                value="{{milestone.estimated_quarter_days}}">
+              <label for="start">Start Date</label>
+              <input id="start" type="date" name="start" />
+
+              <label for="due">Due Date</label>
+              <input id="due" type="date" name="due" />
+              <div>
+                <input type="submit" class="add_button"
+                  value="{% if milestone.name.len() == 0%}Create Milestone!{%else%}Update Milestone{% endif %}" />
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  window.addEventListener('load', function () {
+    {% if milestone.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+    send_delete("delete", "/milestone/{{milestone.key}}", (deleted, res) => {
+      if (deleted) {
+        window.location.href = `/project/{{milestone.project_key}}`
+      }
+    })
+    {% endif %}
+    post_form("add_milestone_form", "/milestone", data => {
+      const key = "{{milestone.key}}"
+      data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+      data.organization_key = "{{user.organization_key}}"
+      data.owner_key = "{{user.key}}"
+      data.project_key = "{{milestone.project_key}}"
+      data.name = data.name || "";
+      data.description = data.description || "";
+      data.tags = data.tags || "";
+      data.estimated_quarter_days = parseInt(data.estimated_quarter_days || "0");
+      data.start = Math.floor(new Date(data.start).getTime() / 1000);
+      data.due = Math.floor(new Date(data.due).getTime() / 1000);
+      data.created = data.created || 0;
+      data.updated = data.updated || 0;
+      return data;
+    }, (response_text) => {
+      const object = JSON.parse(response_text);
+      window.location.href = `/milestone/${object.key}`
+    });
+    let due = parseInt("{{milestone.due}}");
+    document.getElementById("due").value = (due == 0 ? new Date() : new Date(due * 1000)).toISOString().split('T')[0]
+    let start = parseInt("{{milestone.start}}");
+    document.getElementById("start").value = (start == 0 ? new Date() : new Date(start * 1000)).toISOString().split('T')[0]
+  })
+</script>
+{% endblock %}

+ 63 - 0
templates/note.html

@@ -0,0 +1,63 @@
+{% extends "layout.html" %}
+
+{% block title %}{% if note.title.len() > 0 %}{{note.title}}{%else%}Create Note{% endif %}{% endblock %}
+{% block description %}{{note.content}}{% endblock %}
+
+{% block head %}
+<link rel="stylesheet" defer href="https://unpkg.com/easymde/dist/easymde.min.css">
+{% endblock %}
+pub association_type: AssociationType,
+pub association_key: uuid::Uuid,
+
+{% block content %}
+<div class="container">
+    <div class="row justified">
+        <div class="content">
+            <div class="backed col-sm-12 col-md-12 col-lg-12">
+                {% if note.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+                <button id="delete" class="delete_button center">🗑️ Delete</button>
+                {% endif %}
+                <form id="add_note_form">
+                    <h1 name="title" id="title" contentEditable="true">{% if note.title.len() ==
+                        0%}Title{%else%}{{note.title}}{% endif %}</h1>
+                    <textarea name="content" id="note_area_md">{{note.content}}</textarea>
+                    <input type="submit" class="add_button"
+                        value="{% if note.title.len() == 0%}Create Note!{%else%}Update Note{% endif %}" />
+                </form>
+            </div>
+        </div>
+    </div>
+    <script src="/fs/js/easymde@2.18.0.min.js"></script>
+    <script>
+        window.addEventListener('load', function () {
+            {% if note.key.to_string() != "00000000-0000-0000-0000-000000000000" %}
+            send_delete("delete", "/note/{{note.key}}", (deleted, res) => {
+                if (deleted) {
+                    let type = "{{note.association_type.to_string()}}".toLowerCase()
+                    window.location.href = `/${type}/{{note.association_key}}`
+                }
+            })
+            {% endif %}
+            const easyMDE = new EasyMDE({ element: document.getElementById('note_area_md') });
+
+            const title = document.getElementById('title');
+            post_form("add_note_form", "/note", data => {
+                const key = "{{note.key}}"
+                data.key = key == "" ? "00000000-0000-0000-0000-000000000000" : key
+                data.organization_key = "{{user.organization_key}}"
+                data.owner_key = "{{user.key}}"
+                data.association_type = "{{note.association_type.to_string()}}";
+                data.association_key = "{{note.association_key}}";
+                data.url = data.url || "";
+                data.title = title.innerHTML;
+                data.content = data.content || "";
+                data.created = data.created || 0;
+                data.updated = data.updated || 0;
+                return data;
+            }, (response_text) => {
+                const object = JSON.parse(response_text);
+                window.location.href = `/note/${object.key}`
+            });
+        })
+    </script>
+    {% endblock %}

部分文件因为文件数量过多而无法显示