Compare commits
663 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dde2ca06c3 | |||
| 091308787b | |||
| 04f9c456d5 | |||
| 6b7ffe0e26 | |||
| f29940d784 | |||
| 0413daf76b | |||
| 5043749b9f | |||
| 793f475edd | |||
| 03cfbfe4ac | |||
| e8d0360e64 | |||
| 05fd83bd6f | |||
| 864dd9f219 | |||
| 9a9a271b1f | |||
| 89d17107af | |||
| b6ddaae82a | |||
| 0bece2df7d | |||
| 4464802947 | |||
| 7c44238bae | |||
| b45c1cf327 | |||
| f1d7385294 | |||
| 77f5b911b5 | |||
| f8ca825524 | |||
| 6377f3a8b3 | |||
| 533d5dfc98 | |||
| 1ed3816d6f | |||
| 4ff52bd730 | |||
| 02a4c9db53 | |||
| 433de17562 | |||
| 23e4183bec | |||
| 926ddf6d09 | |||
| a6017a8411 | |||
| 608443b77f | |||
| 11e110bc7d | |||
| 806b9545a9 | |||
| b35fece3d5 | |||
| 4468ef1410 | |||
| e45cc2d2aa | |||
| 291bf96d15 | |||
| f0610bdd9c | |||
| ab82c4b300 | |||
| 94eaf0e254 | |||
| 9a6c80f562 | |||
| 9a4b045bdc | |||
| d3cfa6396d | |||
| 60a7362327 | |||
| 7be9f5989e | |||
| 651ee02c11 | |||
| fd9eaa3e83 | |||
| 83f3ccab0e | |||
| ae74f94655 | |||
| 989ca163a3 | |||
| 5b74083b5f | |||
| f88e93a0b9 | |||
| 2cf9bde1af | |||
| e589a35c67 | |||
| 4e5240c5ac | |||
| 5922a7a051 | |||
| 70effdc706 | |||
| a72aca1d05 | |||
| 351d157533 | |||
| 9493fb111f | |||
| a026efce6e | |||
| 1908c84fbe | |||
| f722fd1208 | |||
| 49feb1958c | |||
| e17d395ae7 | |||
| eeeea74bf1 | |||
| 52a626d7b9 | |||
| b8b6c74d86 | |||
| de7a613098 | |||
| b1c9977711 | |||
| a108cb7808 | |||
| 4f4f573955 | |||
| eb77fcfb72 | |||
| 5e59fcbd4f | |||
| 246a48359c | |||
| 40e983664c | |||
| 1eb239bf88 | |||
| d5afdf7691 | |||
| b539eb8575 | |||
| b8f7f44bb0 | |||
| 2249de439e | |||
| 4f358aa0f3 | |||
| 2f3708d40c | |||
| 46c13b5185 | |||
| d70b4d39ab | |||
| a37a2dd043 | |||
| 519fbcf581 | |||
| e74bf260c6 | |||
| 99f20868ad | |||
| b30a045a4e | |||
| b50778d3eb | |||
| 5ed1d7537a | |||
| a2021b74ba | |||
| 80b68fb126 | |||
| f87b05c90f | |||
| 73d73dc61e | |||
| 578cd8b24b | |||
| bae68520a2 | |||
| 6e20d37dda | |||
| cc8eb89ce0 | |||
| 84b83fc3c8 | |||
| 3afdf25012 | |||
| 5c3e78f605 | |||
| 6117a2f3cd | |||
| 997006cdd7 | |||
| 37bc451741 | |||
| 93f90dae68 | |||
| 7da72cf89a | |||
| ded70cd250 | |||
| 31435ef83e | |||
| d75ff0c5ab | |||
| c0eb66fe22 | |||
| c394a61a4e | |||
| 16b7b3bc3e | |||
| 065331c77f | |||
| 2b74c6779a | |||
| 0d66f2c6d3 | |||
| 6d25fb715b | |||
| 0ddfa5f7d9 | |||
| b015887b02 | |||
| 75b11d5744 | |||
| eb026ae4ac | |||
| 38d3b28445 | |||
| 0d0395b4e3 | |||
| e6c6ee441a | |||
| bfb96d536e | |||
| ad66250734 | |||
| acd318b640 | |||
| 55fad7ea27 | |||
| debbf5a5b0 | |||
| dd3078dc69 | |||
| d9f52bb634 | |||
| 04c4890124 | |||
| 1c103337fe | |||
| 13c89c29c5 | |||
| df7a6e18fd | |||
| a8de067810 | |||
| cf7712acea | |||
| 0f258fd55b | |||
| bf2f7d1e16 | |||
| e1b1618015 | |||
| e5d09491d4 | |||
| e1c9e6ccb4 | |||
| 5947e2a664 | |||
| e95c6ce8f4 | |||
| c1e3783272 | |||
| eaf1fc896f | |||
| e97ebfd8c2 | |||
| cc281f3533 | |||
| 4e07b549a2 | |||
| 019cf79704 | |||
| a102843789 | |||
| bf443a65e2 | |||
| 5ac55518ab | |||
| 514f518d2a | |||
| fc75b0cd8e | |||
| 7380c64141 | |||
| 943e7a7317 | |||
| 8fb71366f8 | |||
| 74c95ed34b | |||
| 82b3ea31e6 | |||
| 1b5f217e9c | |||
| ee5a8a05c9 | |||
| 499fb1f7df | |||
| 5f266319a3 | |||
| e32fa059e5 | |||
| 3371b15631 | |||
| c0e407e156 | |||
| d2f4ed93df | |||
| 1d6b0491a7 | |||
| c70bebb2aa | |||
| f7273e5cdf | |||
| 8304666261 | |||
| ec18abc97f | |||
| adbebd4de8 | |||
| e44b4b2540 | |||
| 248cb38390 | |||
| 506c2ab1c9 | |||
| 43bf6696d5 | |||
| f52f00abcf | |||
| da4ea18003 | |||
| 5e5615a2d1 | |||
| fe8347f32a | |||
| d2b360f7b3 | |||
| c9618458cb | |||
| ca918bc7e3 | |||
| c820197667 | |||
| 6c6ca69141 | |||
| 57f62b4797 | |||
| 1811522473 | |||
| 8e500e8bef | |||
| a2abd38ccf | |||
| 7fb0bdc9a5 | |||
| 7e5157f218 | |||
| cead85b165 | |||
| 25d4b9fc0c | |||
| 4cdfd64db0 | |||
| 70c2568af8 | |||
| 0683074b8b | |||
| 8f6c07afd6 | |||
| 3b2f0a633c | |||
| c920a8607c | |||
| eb8d8e84f2 | |||
| 619aa58fb3 | |||
| 7accdc4416 | |||
| ae8e5c4dbc | |||
| 454eb590ce | |||
| 9698c5e40a | |||
| 48680577a8 | |||
| a170e6454f | |||
| 25f90bc625 | |||
| c3f3cc085a | |||
| 9871e4f5d0 | |||
| 84229f1af9 | |||
| 3c3241f93c | |||
| 53dd2c1f1d | |||
| 592151bdb6 | |||
| e494d6e381 | |||
| 05b88c8add | |||
| 7a397c3d13 | |||
| bd5aed7b90 | |||
| 59c8dac851 | |||
| 41a2b7e58e | |||
| 1b86913c00 | |||
| 3e1e0b604f | |||
| 54602b55bb | |||
| 086b985383 | |||
| 25cc0d2447 | |||
| d1afe13a1c | |||
| 9974e0f458 | |||
| 7b32f59281 | |||
| 7389c79c52 | |||
| d5adf8b9f6 | |||
| 47de317b65 | |||
| 9e3252d1b0 | |||
| 2570c3410b | |||
| dc4240e702 | |||
| 997e9422eb | |||
| cb494ac416 | |||
| 078f886f39 | |||
| 15727f716a | |||
| 476898b1d4 | |||
| 1a78e3a5bc | |||
| 86e71c48f5 | |||
| 6ebb29cd22 | |||
| e2315e6a54 | |||
| 61f0c8aa66 | |||
| bf632fad2e | |||
| 8cebd985a2 | |||
| 6ff617a733 | |||
| 4d9956cf65 | |||
| ac3c936820 | |||
| 08fc32b0e1 | |||
| d8ad49d14d | |||
| 3debf75eb9 | |||
| 200b1c304b | |||
| 1b33bb3d1c | |||
| 33fdb2c489 | |||
| cfb6ddfcea | |||
| 8e1ed8bef3 | |||
| 30f22fbd78 | |||
| a155ab6deb | |||
| a4ebb33cd5 | |||
| 64fa1aa8cf | |||
| 120d6ec7d8 | |||
| 6291b59849 | |||
| a96e966911 | |||
| 69aa650203 | |||
| 8028cc764f | |||
| b3869a7833 | |||
| 912a98788e | |||
| f90e9dfab0 | |||
| c43c9458a9 | |||
| 90064a8cf0 | |||
| bacab20f4a | |||
| 7b995044b4 | |||
| e878dca3d7 | |||
| fad9ad2be4 | |||
| 15fe9c20df | |||
| 789ad8257a | |||
| 63a549f0e3 | |||
| d422d4e067 | |||
| 881c0f406b | |||
| ee84731153 | |||
| b62ecc6699 | |||
| 21f8b03483 | |||
| 03406c68d0 | |||
| d3883126bf | |||
| e38333e272 | |||
| 2eb2441f2b | |||
| 7409bba0d8 | |||
| 70940e768c | |||
| 37f93d5127 | |||
| 5517eebafe | |||
| bf357a0e2d | |||
| 02dbe3ef2e | |||
| bf2ceded96 | |||
| 97d1c6f02b | |||
| e0b600b651 | |||
| 3379429329 | |||
| bdca3372f9 | |||
| ca9cc692e4 | |||
| afe1faf214 | |||
| d610d091fe | |||
| 36d7732234 | |||
| ca48f7612a | |||
| 7d50cc40de | |||
| 934385ff55 | |||
| fb9a1a6129 | |||
| 3a60abbac0 | |||
| 8d8f78241d | |||
| 937492f6f5 | |||
| b94160df72 | |||
| 456ebaf423 | |||
| 2d27f53941 | |||
| b6f3160dbc | |||
| 77125f45cc | |||
| d882bbca85 | |||
| ac45307da6 | |||
| ea2b6e3608 | |||
| 3a39d110f0 | |||
| 6d39f2a4cb | |||
| f82736641f | |||
| 3d19313a7f | |||
| 17aae72583 | |||
| a634839678 | |||
| ac3693562b | |||
| 99149d9f2a | |||
| 2b6e17c1ef | |||
| 34421dcd49 | |||
| 48abf57218 | |||
| 867dcb0004 | |||
| c840268678 | |||
| f783b135c7 | |||
| a04fe1aa7e | |||
| d01a2dd651 | |||
| ec8fa56c95 | |||
| 91508c50b5 | |||
| f7b7b63e3f | |||
| bb720c87c1 | |||
| 228862fefa | |||
| 04d85b3c63 | |||
| 5f487e8c79 | |||
| 0dff432337 | |||
| 4295a86e55 | |||
| 3c8cc0b2b6 | |||
| 34df45da2f | |||
| a6068aae1d | |||
| 8ee1a09dc7 | |||
| f593dc8463 | |||
| 129f1bf3da | |||
| 304b43cb30 | |||
| 3a483be258 | |||
| 220d9e3d56 | |||
| fc473f1d11 | |||
| 22b68eefd8 | |||
| 6764a420b0 | |||
| 45df254fe7 | |||
| f3fc8b7072 | |||
| 1634e267b8 | |||
| 267b706544 | |||
| ed6ae7e0d2 | |||
| c71681b197 | |||
| 04b6eb509c | |||
| 7897d8a8ad | |||
| 6378ad2734 | |||
| 6f5ef10553 | |||
| 28ba09e952 | |||
| a86a7dce57 | |||
| 27e79a4693 | |||
| 0cdcec10ca | |||
| 15505ee4a2 | |||
| 31e2669c4d | |||
| 362fc5c2ff | |||
| 79e7d007a4 | |||
| a9fad093e6 | |||
| 56efd2eb3f | |||
| 078cd0050a | |||
| e367f4ca21 | |||
| 0b63e05fe2 | |||
| cf25efb4d9 | |||
| 012138179c | |||
| 8b40778ee1 | |||
| 6ae935309a | |||
| 43ccaf52af | |||
| 82e08d0f69 | |||
| 266d97ad63 | |||
| 6869c6fe6f | |||
| 59d8e9b95e | |||
| 61bfb3cfa8 | |||
| 87648490fd | |||
| 3e392dc3ae | |||
| 35b890aadc | |||
| 1bc8535dbb | |||
| 6f5d93cbbe | |||
| 311a133ab8 | |||
| 52de36b158 | |||
| b51e7ea448 | |||
| 154b6a3bf7 | |||
| 04220ca1e7 | |||
| 8d9e6a2c62 | |||
| e47188eab2 | |||
| 0be619355f | |||
| af4c68feac | |||
| 7838870a0c | |||
| cb037910df | |||
| f3bff76aa1 | |||
| 70be08eaf8 | |||
| 3d04d92aa2 | |||
| 5cd7152ae7 | |||
| 9f85f67031 | |||
| 1315282c7f | |||
| becd086865 | |||
| 487852f07e | |||
| ca45765c46 | |||
| 5a69a61d48 | |||
| 0925899cee | |||
| a9f98adb07 | |||
| 25c12053a6 | |||
| 36564ffbcd | |||
| 2f6e241417 | |||
| 186f2b11c3 | |||
| 3b6e44c331 | |||
| 1ff9950a55 | |||
| a70e9d03ff | |||
| ae1dc1a91e | |||
| 5ff0630726 | |||
| 7d1b471d88 | |||
| 054fc8ef99 | |||
| 03d0114dcd | |||
| b17e01a288 | |||
| f6028f3863 | |||
| 92c98bd986 | |||
| dc8668b904 | |||
| f06a19814b | |||
| 86dc2bf1c9 | |||
| 9440bf47a5 | |||
| 8c65c78904 | |||
| 74c35ad000 | |||
| 85d48c8a71 | |||
| a614f98063 | |||
| 715575001a | |||
| 15a11c3da9 | |||
| b8f874a37d | |||
| 234b3710d4 | |||
| 778346b0b0 | |||
| 5870f04260 | |||
| d5ff4191b6 | |||
| c1fe84521e | |||
| 3daef197e9 | |||
| 9606126196 | |||
| d4c1677e38 | |||
| 9dc38a0803 | |||
| 11dfcdd3d6 | |||
| 1f58b37a5e | |||
| ddfe969d6c | |||
| 98206059fc | |||
| 9d08139f43 | |||
| 844680e573 | |||
| 8601a67a7d | |||
| b1e8f534ef | |||
| aae9b4eb83 | |||
| 0e23f37f57 | |||
| 37ab6eec15 | |||
| 5d75089127 | |||
| beb7a0cfcb | |||
| f244df6293 | |||
| d79bab2997 | |||
| 2538eea177 | |||
| 1d335390c2 | |||
| b9fee95cf2 | |||
| 359aea8308 | |||
| e0bfd220f7 | |||
| c9f9dd3262 | |||
| c1e1506720 | |||
| 46932c91a6 | |||
| 2306a4b2f6 | |||
| 07cd7d2135 | |||
| eee1f31903 | |||
| 88e81d4d80 | |||
| 6137b401ee | |||
| 41ce0a3b80 | |||
| 9ba9858bea | |||
| 5341bbcbe2 | |||
| 4f62a704e2 | |||
| a3f3f51c60 | |||
| a9e9c347f4 | |||
| 9169fbafb2 | |||
| aae62aae08 | |||
| 62ec185154 | |||
| 551b73acfb | |||
| febce4f2e3 | |||
| df2bebaf3d | |||
| b17e4e88f6 | |||
| c327833314 | |||
| 4148d8af80 | |||
| f43a055d63 | |||
| dbcc5d8a97 | |||
| 7f2612d9a6 | |||
| cf03e0e338 | |||
| 17054b396e | |||
| 72bc113b88 | |||
| 33dcbae705 | |||
| cfc1f3eb69 | |||
| 9f6533ece9 | |||
| a291d8a38b | |||
| c1ab27172c | |||
| d10312fe15 | |||
| 5365f31e90 | |||
| 73a6e617bb | |||
| 3f31744911 | |||
| 3b018bc6bf | |||
| 6aa02680d8 | |||
| c926498d3d | |||
| 6fd6f79daf | |||
| aceea56681 | |||
| a786e78aca | |||
| 3bdb9251da | |||
| 6d25e02cb5 | |||
| 8ee4280461 | |||
| 55a1e97778 | |||
| cda0efb731 | |||
| 364198ba4a | |||
| ca30800e6a | |||
| 459bb4531f | |||
| 34a3fe426b | |||
| 53deb0b8cd | |||
| e1c56b2e53 | |||
| 845cf2adba | |||
| 7ed8401088 | |||
| de1a4aad30 | |||
| 6d0dc451e4 | |||
| af15412954 | |||
| 89e5dacca9 | |||
| 7d4a195519 | |||
| c3628813c6 | |||
| d3b48b7695 | |||
| 29131d4ee6 | |||
| cc1e4f916f | |||
| 240f801755 | |||
| 4c7fce0233 | |||
| 5cf504745b | |||
| 9c0f882ba0 | |||
| a40f592aab | |||
| 77947282e4 | |||
| 0b0f4751fa | |||
| 0227bb9828 | |||
| 9d8b5421ed | |||
| dd78fb4340 | |||
| 45d7105ed1 | |||
| c9131b0e89 | |||
| d847b10e32 | |||
| 7039df9af1 | |||
| cd77ebd742 | |||
| eb9d23c796 | |||
| bddca15b69 | |||
| 261695c14c | |||
| b5c317f971 | |||
| 7914261437 | |||
| be95e87e0e | |||
| c719cf7df0 | |||
| 36dab8b518 | |||
| c5373ff2bf | |||
| bd70640794 | |||
| 2bbe21fb89 | |||
| 084642eb0a | |||
| 11d880087d | |||
| 77cafe9c2c | |||
| 3b39f0883c | |||
| cf7a7e25fb | |||
| fb49ad24d5 | |||
| b270159aae | |||
| 44decae557 | |||
| 7f2fd1fdd8 | |||
| 0f3f5e442f | |||
| f19ab21b7d | |||
| ff8e0c6b3d | |||
| b0667e1b78 | |||
| 9554d0bd53 | |||
| e81e4f0ac3 | |||
| 4674d559a4 | |||
| 0c2e5ff0dc | |||
| 39c4452142 | |||
| 2f4cd59ad9 | |||
| 5cacae6cf2 | |||
| 7a1653a2e9 | |||
| 31da4db14f | |||
| b166ceaea7 | |||
| f49b42f70f | |||
| 1eba1730d1 | |||
| 04adf5fdf5 | |||
| ddd3af4b85 | |||
| 6b360d08c1 | |||
| 322b265d7a | |||
| 920dda79b7 | |||
| 1ce522b98a | |||
| 657e96e133 | |||
| 0429bbb19d | |||
| 45dde0cf4a | |||
| 4cdb2f820b | |||
| dcbb5047b1 | |||
| 15202b8675 | |||
| 02ba735ba9 | |||
| 20f874399d | |||
| e2d02bac5a | |||
| f76093690c | |||
| 7640a8cbab | |||
| 3d59cdba10 | |||
| d3855fef3f | |||
| 336d44b00e | |||
| 46d67acf22 | |||
| 702256bcc0 | |||
| 538d08c16c | |||
| 449020c1e8 | |||
| c811849771 | |||
| 6ea4da3bce | |||
| 34cdffda88 | |||
| b1da081ae6 | |||
| 482785c5e6 | |||
| fec391a336 | |||
| ec08f45bf5 | |||
| dd68181a83 | |||
| a8be61cdbb | |||
| 609c57332e | |||
| 5185bf0c7e | |||
| cbd273da2b | |||
| 0c7039de0e | |||
| f110384f11 | |||
| 953d0a2dc0 | |||
| 9694861cb6 | |||
| 34ce114231 | |||
| 3030145b30 | |||
| a9f0fdaf22 | |||
| 73a0a25b6c | |||
| 2786b8f163 | |||
| 9292d5d604 | |||
| 3ba280e10c | |||
| 4eceeaaca4 | |||
| 205aef595b | |||
| a006a93a04 | |||
| 30e80c675e | |||
| dda9114692 | |||
| 16c2dc4a8c | |||
| 7b0bfd9308 | |||
| c6fd9eb9b1 | |||
| 0fb87eba3f | |||
| 1e70ca1a19 | |||
| ccb7306dcb | |||
| 12ff562d31 | |||
| 3884a33b36 | |||
| b5b1930599 | |||
| 61bc012a62 | |||
| 9c42997209 | |||
| 78385a351e | |||
| 32cec5bad0 | |||
| b78172033f | |||
| b0442e0277 | |||
| bca717873e | |||
| 23d2d9109f | |||
| c8138351c9 | |||
| 24a2f472ec | |||
| 40eb1b10fe |
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
@@ -0,0 +1,95 @@
|
||||
name: CI Workflow
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
|
||||
linters:
|
||||
name: Linter Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install linters
|
||||
run: |
|
||||
cd /tmp && go get -u golang.org/x/lint/golint
|
||||
sudo npm install -g jshint
|
||||
env:
|
||||
GO111MODULE: off
|
||||
- name: Run golint
|
||||
run: |
|
||||
export PATH=/home/runner/go/bin:$PATH
|
||||
make lint
|
||||
- name: Run jshint
|
||||
run: jshint ui/static/js/*.js
|
||||
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
go-version: [1.14, 1.15]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run unit tests
|
||||
run: make test
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.5
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install Postgres client
|
||||
run: sudo apt-get install -y postgresql-client
|
||||
- name: Run integration tests
|
||||
run: make integration-test
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PGHOST: 127.0.0.1
|
||||
PGPASSWORD: postgres
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Docker
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
jobs:
|
||||
docker-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate Docker tag
|
||||
id: docker_tag
|
||||
run: |
|
||||
DOCKER_IMAGE=miniflux/miniflux
|
||||
DOCKER_VERSION=dev
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
DOCKER_VERSION=nightly
|
||||
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
|
||||
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_tag.outputs.tags }}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
miniflux-linux-amd64
|
||||
miniflux-linux-arm*
|
||||
miniflux-darwin-amd64
|
||||
miniflux-test
|
||||
miniflux-*
|
||||
miniflux
|
||||
*.rpm
|
||||
*.deb
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
notifications:
|
||||
email: false
|
||||
services:
|
||||
- postgresql
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
language: go
|
||||
go:
|
||||
- 1.9
|
||||
before_install:
|
||||
- npm install -g jshint
|
||||
- go get -u github.com/golang/lint/golint
|
||||
script:
|
||||
- jshint ui/static/js/app.js
|
||||
- make lint
|
||||
- make test
|
||||
- make integration-test
|
||||
@@ -1,3 +1,677 @@
|
||||
Version 2.0.28 (Feb 15, 2021)
|
||||
-----------------------------
|
||||
|
||||
* Add HTTP header "Referrer-Policy: no-referrer"
|
||||
* Handle entry title with double encoded entities
|
||||
* Add Open Containers annotations to Docker image
|
||||
* Remove iframe inner HTML contents (iframe element never has fallback content)
|
||||
* Update date parser to fix another time zone issue
|
||||
* Update German translation for blocklist and keeplist
|
||||
* Validate Keep list and Block list rules syntax
|
||||
* Add support for IPv6 with zone index
|
||||
* Allow images with data URLs
|
||||
* Limit full-text search indexation to first 500K characters (tsvector has a size limit of 1MB)
|
||||
* Change PWA display mode to standalone
|
||||
* ETag value is not set correctly in HTTP client (regression)
|
||||
* Add database backed Let's Encrypt certificate cache
|
||||
* Add global option POLLING_PARSING_ERROR_LIMIT
|
||||
* Update systemd service file comments to use `systemctl edit` for editing
|
||||
* Update Go version to 1.15 in go.mod
|
||||
* Don't discard the "Fetch via Proxy" option
|
||||
* Update man page to show the default values
|
||||
* Add PostgreSQL indices
|
||||
* Add API endpoints to get feeds and entries of a category
|
||||
* Create feed query builder
|
||||
* Bump github.com/PuerkitoBio/goquery from 1.6.0 to 1.6.1
|
||||
* Show global options in the about page
|
||||
* Update man page to mention -1 can be used for CLEANUP_ARCHIVE_* options
|
||||
|
||||
Version 2.0.27 (Jan 9, 2021)
|
||||
----------------------------
|
||||
|
||||
* Add spellcheck="false" to input fields
|
||||
* Refactoring of entry, feed, category, and user validation
|
||||
* Avoid stripping tags for entry title
|
||||
* Add the possibility to subscribe to feeds with the Android Share menu
|
||||
* API improvements:
|
||||
- Change feed creation request to allow setting most fields via API
|
||||
- Allow regular users to change settings via API
|
||||
- Make user fields editable via API
|
||||
- Renaming non-existent category via API should return a 404
|
||||
* Update Systemd service file:
|
||||
- Add capability CAP_NET_BIND_SERVICE (allow the process to listen on privileged ports)
|
||||
- Enable a private /tmp for $CERT_CACHE (required when using Let's Encrypt)
|
||||
* Update read/star icons to SVGs
|
||||
* Add autocomplete="username" to HTML forms
|
||||
* Improve user mass delete to use fewer Goroutines
|
||||
* Use SQL transaction when creating user sessions and users
|
||||
* Remove extra column (HSTORE field) from users table and migrate key/value pairs to specific columns
|
||||
* Bump github.com/prometheus/client_golang from 1.8.0 to 1.9.0
|
||||
* Bump github.com/lib/pq from 1.8.0 to 1.9.0
|
||||
* Add styles for <abbr> HTML tag
|
||||
* Refactor SQL migrations:
|
||||
- Avoid embedding SQL files into binary
|
||||
- Allow more flexible changes by using Go functions
|
||||
* Add Server-Timing header to unread page
|
||||
* Show correct User Agent in input placeholders
|
||||
* Add autocomplete attribute to login form
|
||||
* Add Grafana dashboard in contrib folder
|
||||
|
||||
Version 2.0.26 (Dec 5, 2020)
|
||||
----------------------------
|
||||
|
||||
* Use created_at instead of published_at for archiving entries
|
||||
* Add created_at field for entries
|
||||
* Handle invalid feeds with relative URLs
|
||||
* Add API routes for "mark all as read"
|
||||
* Add support for setting a global default User-Agent
|
||||
* Add rewrite rule "replace" for custom search and replace
|
||||
* Calculate reading time during feed processing
|
||||
* Handle various invalid dates
|
||||
* systemd: keep /run writeable
|
||||
* debian package: add missing post-install script
|
||||
* Do not follow redirects when trying known feed URLs
|
||||
* Trim spaces around icon URLs
|
||||
* Reinstate EXPOSE instruction in Dockerfile
|
||||
* Update German and Portuguese translations
|
||||
|
||||
Version 2.0.25 (Nov 3, 2020)
|
||||
----------------------------
|
||||
|
||||
* Rename "original" link to be more explicit
|
||||
* Do not escape HTML for Atom 1.0 text content during parsing (Avoid HTML entities issues)
|
||||
* Do not use charset.NewReader if the body is a valid UTF-8 document
|
||||
* Restore the ability to use a proxy for all HTTP requests (see https://golang.org/pkg/net/http/#ProxyFromEnvironment)
|
||||
* Show Git commit in about page
|
||||
* Publish Docker images to GitHub Container Registry
|
||||
* Added few Docker Compose examples in contrib folder
|
||||
* Added Ansible Role + Playbook for Miniflux in contrib folder
|
||||
* Add rewrite rule to use noscript content for images rendered with Javascript
|
||||
* Bump github.com/prometheus/client_golang from 1.7.1 to 1.8.0
|
||||
* Update contributor link and Godoc badge for API client
|
||||
* Move Debian package builder to main repository
|
||||
* Move RPM build files to main repository
|
||||
* Add GitHub Action to generate Docker images
|
||||
* Build multi-platform images with Docker Buildx
|
||||
* Add keyboard shortcut to scroll current item to the top
|
||||
* Add feed filters (Keeplist and Blocklist)
|
||||
* Do not proxy image with a data URL
|
||||
* Bump github.com/PuerkitoBio/goquery from 1.5.1 to 1.6.0
|
||||
* Proxify articles crawled manually
|
||||
* Proxify images defined in srcset attribute
|
||||
* Remove plaintext Fever password from database
|
||||
* Add keyboard shortcut to jump to an item's feed page
|
||||
* Add option for swipe gesture on entries on mobile
|
||||
|
||||
Version 2.0.24 (Oct 3, 2020)
|
||||
----------------------------
|
||||
|
||||
* Add rewrite rule to fix Medium.com images
|
||||
* Update sanitizer to support responsive images:
|
||||
- Add support for picture HTML tag
|
||||
- Add support for srcset, media, and sizes attributes to img and source tags
|
||||
* Enhance man page formatting
|
||||
* Add Prometheus exporter
|
||||
* Remove dependency on global config options in HTTP client
|
||||
* API:
|
||||
- Avoid database lookup if empty credentials are provided
|
||||
- Add the possibility to filter entries by category ID
|
||||
- Add the possibility to filter entries by a list of statuses
|
||||
* Add Feed ID in worker error logs
|
||||
* Tweak default HTTP client transport timeout values to reduce the number of file descriptors
|
||||
* CSS tweaks and optimizations:
|
||||
- Prevent sub and sup from affecting line-height
|
||||
- Set touch-action on .items to prevent browser navigation
|
||||
- Move font-family specific CSS to the appropriate file
|
||||
- Update primary font-family for UI to be even more compatible with various operating systems
|
||||
- Make .entry-content font-weight variable depending on font-family used
|
||||
* Avoid Javascript minifier to break keyboard shortcuts
|
||||
* Rename service worker script to avoid being blocked by uBlock
|
||||
* Update date parser to handle Pacific Daylight Time in addition to Pacific Standard Time
|
||||
* Create index to speed up bookmark page
|
||||
* Do not try to update a duplicated feed after a refresh
|
||||
* Use a transaction to refresh and create entries
|
||||
* Speed up entries clean up with an index and a goroutine
|
||||
* Avoid the accumulation of enclosures by keeping only what is referenced in the feed
|
||||
* Add workarounds for parsing an invalid date
|
||||
* Archive older entries first
|
||||
* Update API client to support more filters
|
||||
* Avoid duplication between get feed entries and get entries API endpoints
|
||||
* Enable strict slash to avoid a page not found (404) when using a trailing slash in the URLs
|
||||
* Add a submit button to each section of the integration page
|
||||
* Reload page after making page as read when showing unread entries
|
||||
* Add option to archive unread entries
|
||||
* Add option to enable maintenance mode
|
||||
* Add HTTP proxy option for subscriptions
|
||||
* Make add_invidious_video rule applicable for different invidious instances
|
||||
* Fix reading time for jp, ko and zh languages
|
||||
* Update POLLING_SCHEDULER description in man page
|
||||
* Bump gorilla/mux from 1.7.4 to 1.8.0
|
||||
* Add link to mark a feed as read
|
||||
|
||||
Version 2.0.23 (Aug 15, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Try known URLs when discovering subscriptions
|
||||
* Add workarounds to find YouTube channel feeds (YouTube doesn't expose RSS links anymore for new-style URLs)
|
||||
* Increase HTTP server timeout values
|
||||
* Use stdlib constants for HTTP methods instead of strings
|
||||
* Add support for RTL feed content
|
||||
* Ignore <media:title> to avoid overriding the default title if they are different
|
||||
* Add support for secret keys exposed as a file (useful for containerized environments)
|
||||
* Display recent entries first in search results
|
||||
* Do not archive shared items
|
||||
* Add option to change the number of entries per page
|
||||
* Add Brazilian Portuguese (pt_BR) translation
|
||||
* Add reading time for entries
|
||||
* Redirect to login page if CSRF token is expired
|
||||
* Fever API:
|
||||
- Use getEntryIDs instead of getEntries to reduce memory consumption
|
||||
- Fix max_id argument logic to follow the specs
|
||||
- Improve logging
|
||||
- Do not send articles to external services when unsaving an item
|
||||
- Create index to speed up API calls
|
||||
- Log client IP in middleware
|
||||
* API client: Do not return body for response with no content
|
||||
* REST API:
|
||||
- Delete users asynchronously (Deleting large users might lock the tables)
|
||||
- Add CORS support
|
||||
* Align entry actions to the left
|
||||
- Attempt to avoid awkward alignment on smartphone screens
|
||||
- Keep the read/star actions aligned to the left
|
||||
- Remove CSS flex to allow easier override with custom CSS
|
||||
* Upgrade Postgres client library
|
||||
* Upgrade CI checks to Go 1.15
|
||||
|
||||
Version 2.0.22 (Jun 19, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Remove child-src CSP policy (deprecated)
|
||||
* Add /version endpoint
|
||||
* Add the ability to use custom css
|
||||
* Handle more invalid dates
|
||||
* Add CSS styles for textarea
|
||||
* Add index to speed up slow query
|
||||
* Speed up feed list page rendering
|
||||
* Add alternative scheduler based on the number of entries
|
||||
* Setup Dependabot on GitHub
|
||||
* Update Docker image to Alpine 3.12
|
||||
* Add feed option to ignore HTTP cache
|
||||
* Fix some Italian and German translations
|
||||
* Added scraper rule for RayWenderlich.com, TheOatmeal.com, financialsamurai.com, dilbert.com and turnoff.us
|
||||
* Replace link to categories by a link to the list of entries in "Category > Feeds" page
|
||||
* Change feed title to a link to the original website
|
||||
* Add icons to feeds and categories list
|
||||
* Update dependencies and remove vendor folder
|
||||
|
||||
Version 2.0.21 (Mar 28, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Add SVG icons to entry actions
|
||||
* Add support for Invidious
|
||||
- Embed Invidious player for invidio.us feeds
|
||||
- Add new rewrite rule to use Invidious player for Youtube feeds
|
||||
* Check during application startup if the database schema is up to date
|
||||
* Change default theme for public pages to "System Serif"
|
||||
* Add feature to share an article (create a public link of a feed entry)
|
||||
* Fix SQL injection in full-text search rank ordering
|
||||
* Add generic OpenID Connect provider (OAuth2)
|
||||
* Use more secure TLS configuration for autocert server (increase SSL Labs score from B to A+)
|
||||
* Add feature to create per-application API Keys
|
||||
* Add Go 1.14 to GitHub Actions
|
||||
* Add scraper rule for wdwnt.com
|
||||
* Add API client function to refresh all feeds
|
||||
* Add API endpoint to refresh all feeds
|
||||
* Add Auth Proxy authentication
|
||||
* Use rel=prev/next on pagination links
|
||||
|
||||
Version 2.0.20 (Feb 15, 2020)
|
||||
-----------------------------
|
||||
|
||||
* Add Japanese translation
|
||||
* History: show entries in the order in which they were read
|
||||
* Add button to add to Home screen
|
||||
* Ignore enclosures without URL
|
||||
* Correct spelling of "toggle"
|
||||
* List view: align information to the left side, and the actionable buttons to the right
|
||||
* Redirect to /unread when getting a 404 for an unread expired entry
|
||||
* Do not advance to the next item when using the 'v' shortcut on the list of starred items
|
||||
* Wrap around when navigating with keyboard shortcuts on a list view
|
||||
* Remove unused Feed.Entries and Entry.Category from API client
|
||||
* Add comments link keyboard shortcut
|
||||
* Allow application/xhtml+xml links as comments URL in Atom replies
|
||||
* Allow only absolute URLs in comments URL
|
||||
* Use internal XML workarounds to detect feed format
|
||||
* Make menu consistent across feed pages
|
||||
* Make sure external URLs are not encoded incorrectly by Go template engine
|
||||
* Make sure whitelisted URI schemes are handled properly by the sanitizer
|
||||
* Use white background for favicon (Improve legibility when using a dark theme)
|
||||
* Remove dependency on Sed to build Docker images
|
||||
* Normalize URL query string before executing HTTP requests
|
||||
* Improve Dublin Core support for RDF feeds
|
||||
* Improve Podcast support (iTunes and Google Play feeds)
|
||||
* Add support for Atom 0.3
|
||||
* Add support for Atom "replies" link relation
|
||||
* Return outer HTML when scraping elements
|
||||
* Update scraper rule for "Le Monde"
|
||||
* Filter valid XML characters for UTF-8 XML documents before decoding
|
||||
* Trim spaces for RDF entry links
|
||||
|
||||
Version 2.0.19 (Dec 1, 2019)
|
||||
----------------------------
|
||||
|
||||
* Add shortcut "V" to open original link in current tab
|
||||
* Add the possibility to add rules during feed creation
|
||||
* Wrap attachments into <details> disclosure element
|
||||
* Show attachment size on entry page
|
||||
* Add support of RSS Media elements (group, description, peer link, and thumbnails)
|
||||
* Add rewrite functions: convert_text_link and nl2br
|
||||
* Add scraper rule for openingsource.org
|
||||
* Add Makefile target to build only amd64 Docker image
|
||||
* Make sure to remove integration settings when removing a user
|
||||
* Add API parameter to filter entries by category
|
||||
* Display list of feeds per category
|
||||
* Show the number of read and unread entries for each feed
|
||||
* Make sure settings menu is consistent
|
||||
* Remove fixed table-layout for entry content
|
||||
* Update autocert lib because ACME v1 is EOL
|
||||
* Do not lighten blockquote font color
|
||||
* Update de_DE translation
|
||||
* Send a response when changing status of removed entries in Fever API
|
||||
* Add meta tag to disable Google Translate
|
||||
* Improve storage module
|
||||
* Improve XML decoder to remove illegal characters
|
||||
* Compare Fever token case-insensitively
|
||||
* Make sure integration tests are marked as failed in Github Actions
|
||||
* Add new formats to date parser
|
||||
* Add notification message when using keyboard shortcuts: f, s, and m.
|
||||
* Avoid keyboard shortcuts to conflict with Firefox’s "Find as you type" feature
|
||||
|
||||
Version 2.0.18 (Sep 25, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Add Docker image variant for arm32v7
|
||||
* Add theme variants
|
||||
- Use CSS variables instead of inherence
|
||||
- Rename default theme to "Light - Serif"
|
||||
- Rename Black theme to "Dark - Serif"
|
||||
- Rename "Sans-Serif" theme to "Light - Sans Serif"
|
||||
- Add "System" theme that use system preferences: Dark or Light
|
||||
- Add Serif and Sans-Serif variants for each color theme
|
||||
* Avoid constraint error when having duplicate entries during feed creation
|
||||
* Disable strict XML parsing
|
||||
* Ignore invalid content type
|
||||
* Update man page
|
||||
* Replace Travis by GitHub Actions
|
||||
* Rename cleanup config variables and deprecate old ones
|
||||
- CLEANUP_FREQUENCY_HOURS instead of CLEANUP_FREQUENCY
|
||||
- CLEANUP_ARCHIVE_READ_DAYS instead of ARCHIVE_READ_DAYS
|
||||
* Make configurable the number of days to remove old sessions
|
||||
* Add native lazy loading for images and iframes
|
||||
* Do not buffer responses in the image proxy
|
||||
* Update dependencies
|
||||
* Add Go 1.13 to test matrix
|
||||
* Replace link border by outline to avoid slight content shift
|
||||
* New rewrite function: add_mailto_subject
|
||||
* Import OPML from URL
|
||||
* Fix HTML injection in addImageTitle
|
||||
* Accept HTML entities when parsing XML
|
||||
|
||||
Version 2.0.17 (Aug 3, 2019)
|
||||
----------------------------
|
||||
|
||||
* Update Docker image to Alpine Linux 3.10.1
|
||||
* Pass auth header to manifest request (crossorigin attribute)
|
||||
* Sort feed categories before serialization
|
||||
* Fix syntax errors in man page
|
||||
* Add .search margin-right
|
||||
* Ask for confirmation before flushing history, marking page as read, and mark all as read
|
||||
* Add option to disable feeds
|
||||
|
||||
Version 2.0.16 (Jun 8, 2019)
|
||||
----------------------------
|
||||
|
||||
* Add option to toggle date/time in log messages
|
||||
* Add optional config file parser in addition to environment variables
|
||||
* Make HTTP Client timeout and max body size configurable
|
||||
* Refactor config package:
|
||||
- Parse configuration only once during startup time
|
||||
- Store configuration values in a global variable
|
||||
* Flip behavior of j and k keyboard shortcuts
|
||||
* Bump Postgresql client library to v1.1.1 to bring in SCRAM-SHA-256 authentication
|
||||
* Add option to enable/disable keyboard shortcuts
|
||||
* Add missing translation
|
||||
* Improve page reload when showing unread/all entries:
|
||||
- Show only unread entries = refresh current page
|
||||
- Show all entries = go to next page
|
||||
* Always display feed entries even when there is a feed error
|
||||
* Use loading label instead of saving when submitting login form
|
||||
* Add OPML v1 support during importation
|
||||
* Add 'allow-popups' to iframe sandbox permissions
|
||||
|
||||
Version 2.0.15 (Mar 16, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Move Dockerfile to main repo
|
||||
* Change location of the binary from /usr/local/bin to /usr/bin in Docker image
|
||||
* Add double tap detection for next/previous page navigation
|
||||
* Allow users to disable auto-remove
|
||||
* Make parser compatible with Go 1.12
|
||||
* Add Golang 1.12 to CI
|
||||
* Use feed ID instead of user ID to check entry URLs presence
|
||||
* Fix typo in stylesheet
|
||||
* Sort search results by relevance
|
||||
* Use preferably the published date for Atom feeds
|
||||
* Add Spanish translation
|
||||
* Rename session cookies
|
||||
* Handle the case when application session is expired and not user session
|
||||
|
||||
Version 2.0.14 (Jan 13, 2019)
|
||||
-----------------------------
|
||||
|
||||
* Only attempt to change password if the confirmation field is filled in (Firefox)
|
||||
* Remove URL from client user agent
|
||||
* Make the feed list order case-insensitive
|
||||
* Handle XHTML Summary elements for Atom feeds
|
||||
* Make UTF-8 the default encoding for XML feeds
|
||||
* Add more targets to Makefile
|
||||
* Add -mod=vendor in Makefile
|
||||
* Move health check endpoint from ui package to httpd service
|
||||
* Add workaround for non GMT dates (RFC822, RFC850, and RFC1123)
|
||||
* Make sure `<strong>` elements are bold
|
||||
* Show scrollbars only when necessary for <pre> elements
|
||||
* Add Italian translation
|
||||
* Allow to switch between unread only and all entries on category/feed views
|
||||
* Add function storage.UpdateFeedError()
|
||||
* Add BBC News scraping rule
|
||||
* Ignore JSON feeds from EnsureUnicode()
|
||||
* Preserve category selection when no feed is found
|
||||
* Update XML encoding regex to take single quotes into consideration
|
||||
* Send cli errors to stderr
|
||||
* Update dependencies
|
||||
* Make password prompt compatible with Windows
|
||||
* Make configurable the number of days to archive read items
|
||||
* Change log level to debug when starting workers
|
||||
* Do not show $DATABASE_URL warning when showing application info
|
||||
* Move image proxy filter to template functions
|
||||
* Update scraper rule for lemonde.fr
|
||||
* Refactor manual entry scraper
|
||||
* Apply rewriter rules on manual "Fetch Original Content"
|
||||
* Add Makefile target for current OS and architecture
|
||||
* Improve Makefile
|
||||
|
||||
Version 2.0.13 (Nov 25, 2018)
|
||||
-----------------------------
|
||||
|
||||
* Add man page
|
||||
* Add support for Systemd Socket Activation (experimental)
|
||||
* Add the possiblity to listen on Unix sockets
|
||||
* Add config options to disable HTTP and scheduler services
|
||||
* Archive more read entries in cleanup job
|
||||
* Change default database connection string (backward compatible)
|
||||
* Improve logging messages in ui package
|
||||
* Improve overall Simplified Chinese translations
|
||||
* Improve time since post date displays:
|
||||
- "15 days" now is "15 days" rather than "3 weeks" ago
|
||||
- "32 days" is now "1 month" rather than "2 months" ago
|
||||
- "366 days" is now "1 year" rather than "2 years" ago
|
||||
* Allow the scraper to parse XHTML documents
|
||||
* Remove charset=utf-8 from JSON responses
|
||||
* Ignore hotkeys containing Control, Alt or Meta keys
|
||||
* Handle more encoding conversion edge cases
|
||||
* Disable go test caching
|
||||
* Avoid duplication of ldflags in Makefile
|
||||
* Fix wrong translation key for category pages
|
||||
* Code refactoring:
|
||||
- Simplify application HTTP middlewares
|
||||
- Replace daemon and scheduler package with service package
|
||||
- Move UI middlewares and routes to ui package
|
||||
- Move API middleware and routes to api package
|
||||
- Move Fever middleware and routes to fever package
|
||||
|
||||
Version 2.0.12 (Oct 26, 2018)
|
||||
-----------------------------
|
||||
|
||||
* Add OpenBSD build
|
||||
* Improve logging for OAuth2 callback
|
||||
* Make "g f" go to feed, or list of feeds
|
||||
* Add more details in feed storage errors to facilitate debugging
|
||||
* Add entries storage error to feed errors count
|
||||
* Set arbitrary maximum size for tsvector column
|
||||
* Unsubscribe from feed through link or "#"
|
||||
* Simplify feed entries filtering
|
||||
* Simplify feed fetcher
|
||||
* Simplify feed parser and format detection
|
||||
* Improve unit tests in url package
|
||||
* Add short cli flags -i and -v
|
||||
* Convert text links and line feeds to HTML in YouTube channels
|
||||
* Change link state when marking all entries as read
|
||||
* Add missing package descriptions for GoDoc
|
||||
* Fix typo in license header
|
||||
* Refactor HTTP response builder
|
||||
* Improve Fever API performances when marking a feed or group as read
|
||||
* Set focus on article link when pressing prev/next hotkeys
|
||||
* Improve request package and add more unit tests
|
||||
* Add more unit tests for config package
|
||||
* Simplify locale package usage (refactoring)
|
||||
* Translate application in Russian
|
||||
* Use disclosure widget <details> for advanced feed options
|
||||
* Use unique translation IDs instead of English text as key
|
||||
* Add more unit tests for template functions
|
||||
* Fix invalid output when truncating Unicode text in templates
|
||||
* Add the possibility to override default user agent for each feed
|
||||
* Split Makefile linux targets by architecture
|
||||
* Add compiler, Arch, and OS to info command
|
||||
* Avoid line break between emoji and (un)read/(un)star links
|
||||
* Build Docker image for multiple architectures (amd64, arm32v6, arm64v8)
|
||||
|
||||
Version 2.0.11 (Sep 11, 2018)
|
||||
-----------------------------
|
||||
|
||||
* Set cookie flag `SameSite` to Lax mode
|
||||
* Use predefined ciphers when TLS is configured
|
||||
* Avoid displaying an error when shutting down the daemon
|
||||
* Add "Mark this page as read" to the bottom
|
||||
* Store client IP address in request context
|
||||
* Refactor HTTP context handling
|
||||
* Make user creation via environment variables idempotent
|
||||
* Use regular text version of ✔︎ instead of emoji version on iOS
|
||||
* Add toggle status button to entry page
|
||||
* Migrate to Go Modules and Go 1.11
|
||||
* Show count of feeds with permanent errors in header menu
|
||||
* Display remote client IP in logs when having a login failure (Fail2Ban)
|
||||
* Add remove button in feed edit page
|
||||
* Split integration tests into multiple files
|
||||
* Update scraper rule for heise.de
|
||||
* Expose real error messages for internal server API errors
|
||||
* Move Golang API client in project source tree (the separate project is deprecated)
|
||||
* Use canonical imports
|
||||
* Add Procfile
|
||||
* Create database package (refactoring)
|
||||
* Update user agent with new website URL
|
||||
* Update German translation
|
||||
|
||||
Version 2.0.10 (July 22, 2018)
|
||||
------------------------------
|
||||
|
||||
* Avoid browser caching issues when assets changes
|
||||
* Add Gzip/Deflate compression for HTML, JSON, CSS and Javascript responses
|
||||
* Improve themes handling
|
||||
* Store user theme in session
|
||||
* Logged out users will keep their theme
|
||||
* Add theme background color to web manifest and meta tag
|
||||
* Update application icon with different sizes
|
||||
* Add support for published tag in Atom feeds
|
||||
* Add tooltip to feed domain in feeds list (title attribute)
|
||||
* Prevent vertical scrolling on swipe
|
||||
* Show feed title instead of domain in items list
|
||||
* Add service worker to cache feed icons
|
||||
* Make image proxy configurable via IMAGE_PROXY environment variable:
|
||||
* none = No proxy
|
||||
* http-only = Proxy only non-HTTPS images (default)
|
||||
* all = Proxy everything
|
||||
* Add alt attribute for feed icons
|
||||
* Update CI jshint check
|
||||
* Add embedly.com to iframe whitelist
|
||||
* Use passive event listeners for touch events
|
||||
* Add `add_dynamic_image` rewriter for JavaScript-loaded images
|
||||
* Change feed password field type to text to avoid auto-completion with Firefox
|
||||
* Using autocomplete="off" or autocomplete="new-password" doesn't change anything
|
||||
* Changing the input ID doesn't change anything
|
||||
* Using a different input name doesn't change anything
|
||||
* Only Chrome supports autocomplete="new-password"
|
||||
* Add base URL validation
|
||||
* Update default stylesheet name in HTML layout
|
||||
* Pre-generate themes stylesheets at build time
|
||||
* Update vendor dependencies
|
||||
* Refactor assets bundler and split Javascript files
|
||||
* Run sanitizer after all processing and entry content rewrite
|
||||
* Remove timestamp from generated files
|
||||
* Add support for protocol relative YouTube URLs
|
||||
* Add Postgres full-text search for entries
|
||||
* Add search form in user interface
|
||||
* Add search parameter to the API
|
||||
* Improve Dutch locales
|
||||
* Sandbox iframes when sanitizing
|
||||
* Keep consistent text size on mobile orientation change
|
||||
* Change permission of /etc/miniflux.conf to 600 instead of 644 in RPM package
|
||||
* Add tzdata package to Docker image
|
||||
* Update Docker image to Alpine Linux 3.8
|
||||
|
||||
Version 2.0.9 (July 1, 2018)
|
||||
----------------------------
|
||||
|
||||
* Avoid Chrome to autocomplete no-login password fields
|
||||
* Add cli flag to reset all feed errors
|
||||
* Do not ignore errored feeds when a user refresh feeds manually
|
||||
* Add specific 404 and 401 error messages
|
||||
* Strip binaries to reduce size
|
||||
* Make sure we always get the pagination in unread mode
|
||||
* Fix incorrect data value when toggling bookmark flag on entry page
|
||||
* Set opener to null when opening original URL with JavaScript
|
||||
* Remove unnecessary style
|
||||
* Refactor AddImageTitle rewriter
|
||||
* Only processes images with `src` **and** `title` attributes (others are ignored)
|
||||
* Processes **all** images in the document (not just the first one)
|
||||
* Wraps the image and its title attribute in a `figure` tag with the title attribute's contents in a `figcaption` tag
|
||||
* Improve sanitizer to remove `style`, `noscript` and `script` tag contents
|
||||
* Improve feed and user API updates with optional values
|
||||
* Add new fields for feed username/password
|
||||
* Improve memory usage debug log
|
||||
* Disable keep-alive for HTTP client
|
||||
* Close HTTP response body even for failed requests
|
||||
* Add Sans-Serif theme
|
||||
* Rewrite iframe Youtube URLs to https://www.youtube-nocookie.com
|
||||
* Add more filters for API call `/entries`:
|
||||
* before (unix timestamp)
|
||||
* before_entry_id (int64)
|
||||
* after (unix timestamp)
|
||||
* after_entry_id (int64)
|
||||
* starred (boolean)
|
||||
* Rewrite individual entry pagination SQL queries
|
||||
* Simplify entry query builder
|
||||
* Prevent items from sticking on touchend
|
||||
* Extended horizontal overflow to feed and category views
|
||||
* Translate missing strings
|
||||
* Update German translation
|
||||
|
||||
Version 2.0.8 (June 4, 2018)
|
||||
----------------------------
|
||||
|
||||
* Add Pocket integration
|
||||
* Rewrite RealIP() to avoid returning an empty string
|
||||
* Convert IP field from text to inet type
|
||||
* Improve error handling in integration clients
|
||||
* Make unread counter clickable
|
||||
* Archive read entries automatically after 60 days
|
||||
* Hide horizontal overflow when swiping articles on touch devices
|
||||
* Add API endpoint to get logged user
|
||||
* Fever API: Return response with an empty list if there is no item
|
||||
* Handle feeds with dates formatted as Unix timestamp
|
||||
|
||||
Version 2.0.7 (May 7, 2018)
|
||||
---------------------------
|
||||
|
||||
* Add API endpoint to import OPML file
|
||||
* Make sure to close request body in HTTP client
|
||||
* Do not show save link if no integration is configured
|
||||
* Make sure integrations are configured before to make any HTTP requests
|
||||
* Avoid people to unlink their OAuth2 account without having a local password
|
||||
* Do not use shared variable to translate templates (avoid concurrency issue)
|
||||
* Use vanilla HTTP handlers (refactoring)
|
||||
* Move HTTP client to its own package (refactoring)
|
||||
* Add middleware to read X-Forwarded-Proto header (refactoring)
|
||||
* Use Gorilla middleware (refactoring)
|
||||
* Scrape parent element for iframe
|
||||
* Add SoundCloud and Bandcamp iframe sources
|
||||
|
||||
Version 2.0.6 (Apr 20, 2018)
|
||||
----------------------------
|
||||
|
||||
* Improve graceful shutdown
|
||||
* Simplify Heroku deployment
|
||||
* Display memory usage and some metrics in logs
|
||||
* Increase read/write timeout for HTTP server
|
||||
* Add support for Dublin Core date in RDF feeds
|
||||
* Do not return an error if the user session is not found
|
||||
* Handle some non-english date formats
|
||||
* Add missing French translation
|
||||
* Rename RSS parser getters
|
||||
* Get the right comments URL when having multiple namespaces
|
||||
* Ignore caching headers for feeds that send "Expires: 0"
|
||||
* Update translations
|
||||
|
||||
Version 2.0.5 (Apr 7, 2018)
|
||||
---------------------------
|
||||
|
||||
* Avoid unread counter to be off by one when reading an entry
|
||||
* Add Comments URL to entries
|
||||
* Add FreeBSD build target
|
||||
* Handle RSS author elements with inner HTML
|
||||
* Fix typo in translations
|
||||
* Add Dutch translation
|
||||
* Convert enclosure size field to bigint
|
||||
* Switch CI to Go v1.10
|
||||
* Fix broken OPML import when compiling with Go 1.10
|
||||
|
||||
Version 2.0.4 (Mar 5, 2018)
|
||||
---------------------------
|
||||
|
||||
* Add Simplified Chinese translation
|
||||
* Add Nunux Keeper integration
|
||||
* Filter the list of timezones
|
||||
* Add timezone to entries dates for REST and Fever API
|
||||
* Show last login and session creation date in current timezone
|
||||
* Fix typo in edit user template
|
||||
* Improve parser error messages
|
||||
* Remove parentheses around feed error messages
|
||||
* Support localized feed errors generated by background workers
|
||||
* Print info message if DATABASE_URL is not set
|
||||
|
||||
Version 2.0.3 (Feb 19, 2018)
|
||||
----------------------------
|
||||
|
||||
* Add Polish translation
|
||||
* Change color of <q> tags for black theme
|
||||
* Add database indexes (don't forget to run database migrations)
|
||||
* Handle Atom feeds with HTML title
|
||||
* Strip invalid XML characters to avoid parsing errors
|
||||
* Improve error handling for HTTP client
|
||||
|
||||
Version 2.0.2 (Feb 5, 2018)
|
||||
---------------------------
|
||||
|
||||
* Add support for Let's Encrypt http-01 challenge
|
||||
* Move template functions outside engine (refactoring)
|
||||
* Take timezone into consideration when calculating relative time
|
||||
* Add support for HTTP Strict Transport Security header
|
||||
* Add support for base URLs with subfolders
|
||||
* Add missing about menu in settings
|
||||
* Show API URL endpoints in user interface
|
||||
* Do not update entry date while refreshing a feed
|
||||
* Add flag to toggle debug logging
|
||||
* Improve unread counter updates
|
||||
|
||||
Version 2.0.1 (Jan 22, 2018)
|
||||
----------------------------
|
||||
|
||||
|
||||
Generated
-105
@@ -1,105 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/PuerkitoBio/goquery"
|
||||
packages = ["."]
|
||||
revision = "e1271ee34c6a305e38566ecd27ae374944907ee9"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/andybalholm/cascadia"
|
||||
packages = ["."]
|
||||
revision = "349dd0209470eabd9514242c688c403c0926d266"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [".","hstore","oid"]
|
||||
revision = "83612a56d3dd153a94a629cd64925371c9adad78"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/miniflux/miniflux-go"
|
||||
packages = ["."]
|
||||
revision = "887ba3b062946784f0e64edb1734f435beb204f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/tdewolff/minify"
|
||||
packages = [".","css","js"]
|
||||
revision = "222672169d634c440a73abc47685074e1a9daa60"
|
||||
version = "v2.3.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/tdewolff/parse"
|
||||
packages = [".","buffer","css","js","strconv"]
|
||||
revision = "639f6272aec6b52094db77b9ec488214b0b4b1a1"
|
||||
version = "v2.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tomasen/realip"
|
||||
packages = ["."]
|
||||
revision = "b5850897b7b539a1c9f22cdaa3b547d1bd453db8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["acme","acme/autocert","bcrypt","blowfish","ssh/terminal"]
|
||||
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp","html","html/atom","html/charset"]
|
||||
revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","internal"]
|
||||
revision = "462316686f20eb6df426961c1c131bdaa5dfa68e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "571f7bbbe08da2a8955aed9d4db316e78630e9a3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
packages = ["encoding","encoding/charmap","encoding/htmlindex","encoding/internal","encoding/internal/identifier","encoding/japanese","encoding/korean","encoding/simplifiedchinese","encoding/traditionalchinese","encoding/unicode","internal/gen","internal/tag","internal/utf8internal","language","runes","transform","unicode/cldr"]
|
||||
revision = "d5a9226ed7dd70cade6ccae9d37517fe14dd9fee"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "338222e5111416c46b2b8bde149443abc542b386dd02aff2a0dd6e13334bcf28"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/PuerkitoBio/goquery"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "1.6.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/miniflux/miniflux-go"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tdewolff/minify"
|
||||
version = "2.3.3"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tomasen/realip"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/oauth2"
|
||||
@@ -1,53 +1,162 @@
|
||||
APP := miniflux
|
||||
VERSION=$(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE=`date +%FT%T%z`
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/miniflux
|
||||
VERSION := $(shell git describe --tags --abbrev=0)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/version.Version=$(VERSION)' -X 'miniflux.app/version.Commit=$(COMMIT)' -X 'miniflux.app/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
DEB_IMG_ARCH := amd64
|
||||
|
||||
.PHONY: linux linux-arm darwin build run clean test lint integration-test clean-integration-test
|
||||
export PGPASSWORD := postgres
|
||||
|
||||
linux:
|
||||
.PHONY: generate \
|
||||
miniflux \
|
||||
linux-amd64 \
|
||||
linux-arm64 \
|
||||
linux-armv7 \
|
||||
linux-armv6 \
|
||||
linux-armv5 \
|
||||
linux-x86 \
|
||||
darwin-amd64 \
|
||||
freebsd-amd64 \
|
||||
freebsd-x86 \
|
||||
openbsd-amd64 \
|
||||
openbsd-x86 \
|
||||
netbsd-x86 \
|
||||
netbsd-amd64 \
|
||||
windows-amd64 \
|
||||
windows-x86 \
|
||||
build \
|
||||
run \
|
||||
clean \
|
||||
test \
|
||||
lint \
|
||||
integration-test \
|
||||
clean-integration-test \
|
||||
docker-image \
|
||||
docker-images \
|
||||
rpm \
|
||||
debian \
|
||||
debian-packages
|
||||
|
||||
generate:
|
||||
@ go generate
|
||||
@ GOOS=linux GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-amd64 main.go
|
||||
|
||||
linux-arm:
|
||||
@ go generate
|
||||
@ GOOS=linux GOARCH=arm64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv8 main.go
|
||||
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv7 main.go
|
||||
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv6 main.go
|
||||
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv5 main.go
|
||||
miniflux: generate
|
||||
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
darwin:
|
||||
@ go generate
|
||||
@ GOOS=darwin GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-darwin-amd64 main.go
|
||||
linux-amd64: generate
|
||||
@ GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
|
||||
|
||||
build: linux linux-arm darwin
|
||||
linux-arm64: generate
|
||||
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-arm64 main.go
|
||||
|
||||
run:
|
||||
@ go generate
|
||||
@ go run main.go
|
||||
linux-armv7: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
|
||||
|
||||
linux-armv6: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv6 main.go
|
||||
|
||||
linux-armv5: generate
|
||||
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv5 main.go
|
||||
|
||||
darwin-amd64: generate
|
||||
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-amd64 main.go
|
||||
|
||||
freebsd-amd64: generate
|
||||
@ GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-amd64 main.go
|
||||
|
||||
openbsd-amd64: generate
|
||||
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-openbsd-amd64 main.go
|
||||
|
||||
windows-amd64: generate
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-amd64 main.go
|
||||
|
||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
|
||||
# NOTE: unsupported targets
|
||||
netbsd-amd64: generate
|
||||
@ GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-amd64 main.go
|
||||
|
||||
linux-x86: generate
|
||||
@ GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-x86 main.go
|
||||
|
||||
freebsd-x86: generate
|
||||
@ GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
|
||||
netbsd-x86: generate
|
||||
@ GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-x86 main.go
|
||||
|
||||
openbsd-x86: generate
|
||||
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
|
||||
|
||||
windows-x86: generate
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
|
||||
|
||||
run: generate
|
||||
@ LOG_DATE_TIME=1 go run main.go -debug
|
||||
|
||||
clean:
|
||||
@ rm -f $(APP)-*
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
|
||||
|
||||
test:
|
||||
go test -cover -race ./...
|
||||
go test -cover -race -count=1 ./...
|
||||
|
||||
lint:
|
||||
@ golint -set_exit_status ${PKG_LIST}
|
||||
golint -set_exit_status ${PKG_LIST}
|
||||
|
||||
integration-test:
|
||||
psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
psql -U postgres -c 'create database miniflux_test;'
|
||||
DATABASE_URL=$(DB_URL) go run main.go -migrate
|
||||
DATABASE_URL=$(DB_URL) ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go -create-admin
|
||||
go build -o miniflux-test main.go
|
||||
DATABASE_URL=$(DB_URL) ./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
while ! echo exit | nc localhost 8080; do sleep 1; done >/dev/null
|
||||
go test -v -tags=integration || cat /tmp/miniflux.log
|
||||
|
||||
DATABASE_URL=$(DB_URL) \
|
||||
ADMIN_USERNAME=admin \
|
||||
ADMIN_PASSWORD=test123 \
|
||||
CREATE_ADMIN=1 \
|
||||
RUN_MIGRATIONS=1 \
|
||||
DEBUG=1 \
|
||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
|
||||
while ! nc -z localhost 8080; do sleep 1; done
|
||||
go test -v -tags=integration -count=1 miniflux.app/tests
|
||||
|
||||
clean-integration-test:
|
||||
@ kill -9 `cat /tmp/miniflux.pid`
|
||||
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
|
||||
@ rm miniflux-test
|
||||
@ psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
|
||||
docker-image:
|
||||
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/Dockerfile .
|
||||
|
||||
docker-images:
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
|
||||
--file packaging/docker/Dockerfile \
|
||||
--tag $(DOCKER_IMAGE):$(VERSION) \
|
||||
--push .
|
||||
|
||||
rpm: clean
|
||||
@ docker build \
|
||||
-t miniflux-rpm-builder \
|
||||
-f packaging/rpm/Dockerfile \
|
||||
.
|
||||
@ docker run --rm \
|
||||
-v ${PWD}:/root/rpmbuild/RPMS/x86_64 miniflux-rpm-builder \
|
||||
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
|
||||
|
||||
debian:
|
||||
@ docker build \
|
||||
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
|
||||
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
|
||||
-f packaging/debian/Dockerfile \
|
||||
.
|
||||
@ docker run --rm \
|
||||
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
|
||||
|
||||
debian-packages: clean
|
||||
$(MAKE) debian DEB_IMG_ARCH=amd64
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm64v8
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm32v7
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
Miniflux 2
|
||||
==========
|
||||
[](https://travis-ci.org/miniflux/miniflux)
|
||||
[](https://godoc.org/github.com/miniflux/miniflux)
|
||||
[](https://docs.miniflux.net/)
|
||||
|
||||
Miniflux is a minimalist and opinionated feed reader:
|
||||
|
||||
@@ -11,26 +8,47 @@ Miniflux is a minimalist and opinionated feed reader:
|
||||
- Doesn't use any ORM
|
||||
- Doesn't use any complicated framework
|
||||
- Use only modern vanilla Javascript (ES6 and Fetch API)
|
||||
- Single binary compiled statically without dependency
|
||||
- The number of features is voluntarily limited
|
||||
|
||||
It's simple, fast, lightweight and super easy to install.
|
||||
|
||||
Miniflux 2 is a rewrite of [Miniflux 1.x](https://github.com/miniflux/miniflux-legacy) in Golang.
|
||||
Official website: <https://miniflux.app>
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The Miniflux documentation is available here: <https://docs.miniflux.net/>
|
||||
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
|
||||
|
||||
- [Opinionated?](https://docs.miniflux.net/en/latest/opinionated.html)
|
||||
- [Features](https://docs.miniflux.net/en/latest/features.html)
|
||||
- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
|
||||
- [Installation](https://docs.miniflux.net/en/latest/installation.html)
|
||||
- [Upgrading to a new version](https://docs.miniflux.net/en/latest/upgrade.html)
|
||||
- [Configuration](https://docs.miniflux.net/en/latest/configuration.html)
|
||||
- [Opinionated?](https://miniflux.app/opinionated.html)
|
||||
- [Features](https://miniflux.app/features.html)
|
||||
- [Requirements](https://miniflux.app/docs/requirements.html)
|
||||
- [Installation Instructions](https://miniflux.app/docs/installation.html)
|
||||
- [Upgrading to a New Version](https://miniflux.app/docs/upgrade.html)
|
||||
- [Configuration](https://miniflux.app/docs/configuration.html)
|
||||
- [Command Line Usage](https://miniflux.app/docs/cli.html)
|
||||
- [User Interface Usage](https://miniflux.app/docs/ui.html)
|
||||
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
|
||||
- [Integration with External Services](https://miniflux.app/docs/services.html)
|
||||
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
|
||||
- [API Reference](https://miniflux.app/docs/api.html)
|
||||
- [Development](https://miniflux.app/docs/development.html)
|
||||
- [Internationalization](https://miniflux.app/docs/i18n.html)
|
||||
- [Frequently Asked Questions](https://miniflux.app/faq.html)
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
Default theme:
|
||||
|
||||

|
||||
|
||||
Dark theme when using keyboard navigation:
|
||||
|
||||

|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
- Author: Frédéric Guillot
|
||||
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
|
||||
- Distributed under Apache 2.0 License
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/worker"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
store *storage.Storage
|
||||
pool *worker.Pool
|
||||
}
|
||||
|
||||
// Serve declares API routes for the application.
|
||||
func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
handler := &handler{store, pool}
|
||||
|
||||
sr := router.PathPrefix("/v1").Subrouter()
|
||||
middleware := newMiddleware(store)
|
||||
sr.Use(middleware.handleCORS)
|
||||
sr.Use(middleware.apiKeyAuth)
|
||||
sr.Use(middleware.basicAuth)
|
||||
sr.Methods(http.MethodOptions)
|
||||
sr.HandleFunc("/users", handler.createUser).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/users", handler.users).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/users/{userID:[0-9]+}/mark-all-as-read", handler.markUserAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/users/{username}", handler.userByUsername).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/me", handler.currentUser).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories", handler.createCategory).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/categories", handler.getCategories).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/categories/{categoryID}/feeds", handler.getCategoryFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}/entries", handler.getCategoryEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/categories/{categoryID}/entries/{entryID}", handler.getCategoryEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}/refresh", handler.refreshFeed).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete)
|
||||
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries/{entryID}", handler.getFeedEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
|
||||
}
|
||||
+89
-67
@@ -2,102 +2,124 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
// CreateCategory is the API handler to create a new category.
|
||||
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
category, err := decodeCategoryPayload(request.Body())
|
||||
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.store.CreateCategory(userID, &categoryRequest)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
category.UserID = userID
|
||||
if err := category.ValidateCategoryCreation(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
|
||||
response.JSON().BadRequest(errors.New("This category already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.CreateCategory(category)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(category)
|
||||
json.Created(w, r, category)
|
||||
}
|
||||
|
||||
// UpdateCategory is the API handler to update a category.
|
||||
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := decodeCategoryPayload(request.Body())
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
categoryRequest.Patch(category)
|
||||
err = h.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
category.UserID = ctx.UserID()
|
||||
category.ID = categoryID
|
||||
if err := category.ValidateCategoryModification(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(category)
|
||||
json.Created(w, r, category)
|
||||
}
|
||||
|
||||
// GetCategories is the API handler to get a list of categories for a given user.
|
||||
func (c *Controller) GetCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
categories, err := c.store.Categories(ctx.UserID())
|
||||
func (h *handler) markCategoryAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch categories"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(categories)
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.store.MarkCategoryAsRead(userID, categoryID, time.Now()); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// RemoveCategory is the API handler to remove a category.
|
||||
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
categoryID, err := request.IntegerParam("categoryID")
|
||||
func (h *handler) getCategories(w http.ResponseWriter, r *http.Request) {
|
||||
categories, err := h.store.Categories(request.UserID(r))
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.CategoryExists(userID, categoryID) {
|
||||
response.JSON().NotFound(errors.New("Category not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveCategory(userID, categoryID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to remove this category"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.OK(w, r, categories)
|
||||
}
|
||||
|
||||
func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
if !h.store.CategoryIDExists(userID, categoryID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.RemoveCategory(userID, categoryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// Controller holds all handlers for the API.
|
||||
type Controller struct {
|
||||
store *storage.Storage
|
||||
feedHandler *feed.Handler
|
||||
}
|
||||
|
||||
// NewController creates a new controller.
|
||||
func NewController(store *storage.Storage, feedHandler *feed.Handler) *Controller {
|
||||
return &Controller{store: store, feedHandler: feedHandler}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package api implements API endpoints for Miniflux application.
|
||||
|
||||
*/
|
||||
package api
|
||||
+129
-143
@@ -2,222 +2,208 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
// GetFeedEntry is the API handler to get a single feed entry.
|
||||
func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) {
|
||||
entry, err := b.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
if entry == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
json.OK(w, r, entry)
|
||||
}
|
||||
|
||||
func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(entry)
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
// GetEntry is the API handler to get a single entry.
|
||||
func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
response.JSON().NotFound(errors.New("Entry not found"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(entry)
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
// GetFeedEntries is the API handler to get all feed entries.
|
||||
func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
|
||||
builder.WithEntryID(entryID)
|
||||
|
||||
status := request.QueryStringParam("status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
getEntryFromBuilder(w, r, builder)
|
||||
}
|
||||
|
||||
func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
h.findEntries(w, r, feedID, 0)
|
||||
}
|
||||
|
||||
func (h *handler) getCategoryEntries(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
h.findEntries(w, r, 0, categoryID)
|
||||
}
|
||||
|
||||
func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
|
||||
h.findEntries(w, r, 0, 0)
|
||||
}
|
||||
|
||||
func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64, categoryID int64) {
|
||||
statuses := request.QueryStringParamList(r, "status")
|
||||
for _, status := range statuses {
|
||||
if err := validator.ValidateEntryStatus(status); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
order := request.QueryStringParam(r, "order", model.DefaultSortingOrder)
|
||||
if err := validator.ValidateEntryOrder(order); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection)
|
||||
if err := validator.ValidateDirection(direction); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntegerParam("limit", 100)
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
limit := request.QueryIntParam(r, "limit", 100)
|
||||
offset := request.QueryIntParam(r, "offset", 0)
|
||||
if err := validator.ValidateRange(offset, limit); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
userID := request.UserID(r)
|
||||
categoryID = request.QueryInt64Param(r, "category_id", categoryID)
|
||||
if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) {
|
||||
json.BadRequest(w, r, errors.New("Invalid category ID"))
|
||||
return
|
||||
}
|
||||
|
||||
feedID = request.QueryInt64Param(r, "feed_id", feedID)
|
||||
if feedID > 0 && !h.store.FeedExists(userID, feedID) {
|
||||
json.BadRequest(w, r, errors.New("Invalid feed ID"))
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithFeedID(feedID)
|
||||
builder.WithStatus(status)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithStatuses(statuses)
|
||||
builder.WithOrder(order)
|
||||
builder.WithDirection(direction)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(limit)
|
||||
configureFilters(builder, r)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
|
||||
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
||||
}
|
||||
|
||||
// GetEntries is the API handler to fetch entries.
|
||||
func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
|
||||
status := request.QueryStringParam("status", "")
|
||||
if status != "" {
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
order := request.QueryStringParam("order", model.DefaultSortingOrder)
|
||||
if err := model.ValidateEntryOrder(order); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
func (h *handler) setEntryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var entriesStatusUpdateRequest model.EntriesStatusUpdateRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&entriesStatusUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
|
||||
if err := model.ValidateDirection(direction); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
if err := validator.ValidateEntriesStatusUpdateRequest(&entriesStatusUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
limit := request.QueryIntegerParam("limit", 100)
|
||||
offset := request.QueryIntegerParam("offset", 0)
|
||||
if err := model.ValidateRange(offset, limit); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
if err := h.store.SetEntriesStatus(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(status)
|
||||
builder.WithOrder(order)
|
||||
builder.WithDirection(direction)
|
||||
builder.WithOffset(offset)
|
||||
builder.WithLimit(limit)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
count, err := builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// SetEntryStatus is the API handler to change the status of entries.
|
||||
func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
|
||||
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Invalid JSON payload"))
|
||||
func (h *handler) toggleBookmark(w http.ResponseWriter, r *http.Request) {
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
if err := h.store.ToggleBookmark(request.UserID(r), entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := model.ValidateEntryStatus(status); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to change entries status"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// ToggleBookmark is the API handler to toggle bookmark status.
|
||||
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
entryID, err := request.IntegerParam("entryID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) {
|
||||
beforeEntryID := request.QueryInt64Param(r, "before_entry_id", 0)
|
||||
if beforeEntryID > 0 {
|
||||
builder.BeforeEntryID(beforeEntryID)
|
||||
}
|
||||
|
||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
|
||||
return
|
||||
afterEntryID := request.QueryInt64Param(r, "after_entry_id", 0)
|
||||
if afterEntryID > 0 {
|
||||
builder.AfterEntryID(afterEntryID)
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
beforeTimestamp := request.QueryInt64Param(r, "before", 0)
|
||||
if beforeTimestamp > 0 {
|
||||
builder.BeforeDate(time.Unix(beforeTimestamp, 0))
|
||||
}
|
||||
|
||||
afterTimestamp := request.QueryInt64Param(r, "after", 0)
|
||||
if afterTimestamp > 0 {
|
||||
builder.AfterDate(time.Unix(afterTimestamp, 0))
|
||||
}
|
||||
|
||||
categoryID := request.QueryInt64Param(r, "category_id", 0)
|
||||
if categoryID > 0 {
|
||||
builder.WithCategoryID(categoryID)
|
||||
}
|
||||
|
||||
if request.HasQueryParam(r, "starred") {
|
||||
builder.WithStarred()
|
||||
}
|
||||
|
||||
searchQuery := request.QueryStringParam(r, "search", "")
|
||||
if searchQuery != "" {
|
||||
builder.WithSearchQuery(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
+137
-125
@@ -2,191 +2,203 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
feedHandler "miniflux.app/reader/handler"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
// CreateFeed is the API handler to create a new feed.
|
||||
func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedURL, categoryID, err := decodeFeedCreationPayload(request.Body())
|
||||
func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var feedCreationRequest model.FeedCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feedURL == "" {
|
||||
response.JSON().BadRequest(errors.New("The feed_url is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if categoryID <= 0 {
|
||||
response.JSON().BadRequest(errors.New("The category_id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.FeedURLExists(userID, feedURL) {
|
||||
response.JSON().BadRequest(errors.New("This feed_url already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.CategoryExists(userID, categoryID) {
|
||||
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
type result struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
response.JSON().Created(&result{FeedID: feed.ID})
|
||||
json.Created(w, r, &feedCreationResponse{FeedID: feed.ID})
|
||||
}
|
||||
|
||||
// RefreshFeed is the API handler to refresh a feed.
|
||||
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
userID := request.UserID(r)
|
||||
|
||||
if !h.store.FeedExists(userID, feedID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
err := feedHandler.RefreshFeed(h.store, userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.FeedExists(userID, feedID) {
|
||||
response.JSON().NotFound(errors.New("Unable to find this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.feedHandler.RefreshFeed(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to refresh this feed"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// UpdateFeed is the API handler that is used to update a feed.
|
||||
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
jobs, err := h.store.NewUserBatch(userID, h.store.CountFeeds(userID))
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
newFeed, err := decodeFeedModificationPayload(request.Body())
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
go func() {
|
||||
h.pool.Push(jobs)
|
||||
}()
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
var feedModificationRequest model.FeedModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&feedModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
|
||||
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
|
||||
return
|
||||
}
|
||||
userID := request.UserID(r)
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
originalFeed, err := c.store.FeedByID(userID, feedID)
|
||||
originalFeed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().NotFound(errors.New("Unable to find this feed"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if originalFeed == nil {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed.Merge(newFeed)
|
||||
if err := c.store.UpdateFeed(originalFeed); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this feed"))
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed, err = c.store.FeedByID(userID, feedID)
|
||||
feedModificationRequest.Patch(originalFeed)
|
||||
if err := h.store.UpdateFeed(originalFeed); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
originalFeed, err = h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(originalFeed)
|
||||
json.Created(w, r, originalFeed)
|
||||
}
|
||||
|
||||
// GetFeeds is the API handler that get all feeds that belongs to the given user.
|
||||
func (c *Controller) GetFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
feeds, err := c.store.Feeds(ctx.UserID())
|
||||
func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
userID := request.UserID(r)
|
||||
|
||||
feed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch feeds from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(feeds)
|
||||
}
|
||||
|
||||
// Export is the API handler that incoves an OPML export.
|
||||
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
opmlHandler := opml.NewHandler(c.store)
|
||||
|
||||
opml, err := opmlHandler.Export(ctx.LoggedUser().ID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("unable to export feeds to OPML"))
|
||||
}
|
||||
|
||||
response.XML().Serve(opml)
|
||||
}
|
||||
|
||||
// GetFeed is the API handler to get a feed.
|
||||
func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
feed, err := c.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(feed)
|
||||
if err := h.store.MarkFeedAsRead(userID, feedID, time.Now()); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// RemoveFeed is the API handler to remove a feed.
|
||||
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
func (h *handler) getCategoryFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
|
||||
category, err := h.store.Category(userID, categoryID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.FeedExists(userID, feedID) {
|
||||
response.JSON().NotFound(errors.New("Feed not found"))
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveFeed(userID, feedID); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to remove this feed"))
|
||||
feeds, err := h.store.FeedsByCategoryWithCounters(userID, categoryID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
json.OK(w, r, feeds)
|
||||
}
|
||||
|
||||
func (h *handler) getFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
feeds, err := h.store.Feeds(request.UserID(r))
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, feeds)
|
||||
}
|
||||
|
||||
func (h *handler) getFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
feed, err := h.store.FeedByID(request.UserID(r), feedID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if feed == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, feed)
|
||||
}
|
||||
|
||||
func (h *handler) removeFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
userID := request.UserID(r)
|
||||
|
||||
if !h.store.FeedExists(userID, feedID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.RemoveFeed(userID, feedID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
+13
-18
@@ -2,40 +2,35 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
)
|
||||
|
||||
// FeedIcon returns a feed icon.
|
||||
func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
feedID, err := request.IntegerParam("feedID")
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
if !h.store.HasIcon(feedID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !c.store.HasIcon(feedID) {
|
||||
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
|
||||
return
|
||||
}
|
||||
|
||||
icon, err := c.store.IconByFeedID(userID, feedID)
|
||||
icon, err := h.store.IconByFeedID(request.UserID(r), feedID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if icon == nil {
|
||||
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(&feedIcon{
|
||||
json.OK(w, r, &feedIconResponse{
|
||||
ID: icon.ID,
|
||||
MimeType: icon.MimeType,
|
||||
Data: icon.DataURL(),
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/storage"
|
||||
)
|
||||
|
||||
type middleware struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func newMiddleware(s *storage.Storage) *middleware {
|
||||
return &middleware{s}
|
||||
}
|
||||
func (m *middleware) handleCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.ClientIP(r)
|
||||
token := r.Header.Get("X-Auth-Token")
|
||||
|
||||
if token == "" {
|
||||
logger.Debug("[API][TokenAuth] [ClientIP=%s] No API Key provided, go to the next middleware", clientIP)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByAPIKey(token)
|
||||
if err != nil {
|
||||
logger.Error("[API][TokenAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API][TokenAuth] [ClientIP=%s] No user found with the given API key", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API][TokenAuth] [ClientIP=%s] User authenticated: %s", clientIP, user.Username)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
m.store.SetAPIKeyUsedTimestamp(user.ID, token)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
|
||||
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
|
||||
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
|
||||
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *middleware) basicAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if request.IsAuthenticated(r) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
clientIP := request.ClientIP(r)
|
||||
username, password, authOK := r.BasicAuth()
|
||||
if !authOK {
|
||||
logger.Debug("[API][BasicAuth] [ClientIP=%s] No authentication headers sent", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Empty username or password", clientIP)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.store.CheckPassword(username, password); err != nil {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
logger.Error("[API][BasicAuth] %v", err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Error("[API][BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username)
|
||||
json.Unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[API][BasicAuth] [ClientIP=%s] User authenticated: %s", clientIP, username)
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
|
||||
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
|
||||
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
|
||||
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/http/response/xml"
|
||||
"miniflux.app/reader/opml"
|
||||
)
|
||||
|
||||
func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
opmlHandler := opml.NewHandler(h.store)
|
||||
opml, err := opmlHandler.Export(request.UserID(r))
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
xml.OK(w, r, opml)
|
||||
}
|
||||
|
||||
func (h *handler) importFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
opmlHandler := opml.NewHandler(h.store)
|
||||
err := opmlHandler.Import(request.UserID(r), r.Body)
|
||||
defer r.Body.Close()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.Created(w, r, map[string]string{"message": "Feeds imported successfully"})
|
||||
}
|
||||
+5
-82
@@ -2,17 +2,13 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"miniflux.app/model"
|
||||
)
|
||||
|
||||
type feedIcon struct {
|
||||
type feedIconResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Data string `json:"data"`
|
||||
@@ -23,79 +19,6 @@ type entriesResponse struct {
|
||||
Entries model.Entries `json:"entries"`
|
||||
}
|
||||
|
||||
func decodeUserPayload(data io.Reader) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func decodeURLPayload(data io.Reader) (string, error) {
|
||||
type payload struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return p.URL, nil
|
||||
}
|
||||
|
||||
func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
|
||||
type payload struct {
|
||||
EntryIDs []int64 `json:"entry_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return p.EntryIDs, p.Status, nil
|
||||
}
|
||||
|
||||
func decodeFeedCreationPayload(data io.Reader) (string, int64, error) {
|
||||
type payload struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
}
|
||||
|
||||
var p payload
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
|
||||
}
|
||||
|
||||
return p.FeedURL, p.CategoryID, nil
|
||||
}
|
||||
|
||||
func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
|
||||
var feed model.Feed
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&feed); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &feed, nil
|
||||
}
|
||||
|
||||
func decodeCategoryPayload(data io.Reader) (*model.Category, error) {
|
||||
var category model.Category
|
||||
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&category); err != nil {
|
||||
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
|
||||
}
|
||||
|
||||
return &category, nil
|
||||
type feedCreationResponse struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
+27
-15
@@ -2,34 +2,46 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/reader/subscription"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/reader/subscription"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
// GetSubscriptions is the API handler to find subscriptions.
|
||||
func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
websiteURL, err := decodeURLPayload(request.Body())
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
var subscriptionDiscoveryRequest model.SubscriptionDiscoveryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&subscriptionDiscoveryRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, err := subscription.FindSubscriptions(websiteURL)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
|
||||
if validationErr := validator.ValidateSubscriptionDiscovery(&subscriptionDiscoveryRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
subscriptions, finderErr := subscription.FindSubscriptions(
|
||||
subscriptionDiscoveryRequest.URL,
|
||||
subscriptionDiscoveryRequest.UserAgent,
|
||||
subscriptionDiscoveryRequest.Username,
|
||||
subscriptionDiscoveryRequest.Password,
|
||||
subscriptionDiscoveryRequest.FetchViaProxy,
|
||||
)
|
||||
if finderErr != nil {
|
||||
json.ServerError(w, r, finderErr)
|
||||
return
|
||||
}
|
||||
|
||||
if subscriptions == nil {
|
||||
response.JSON().NotFound(fmt.Errorf("No subscription found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(subscriptions)
|
||||
json.OK(w, r, subscriptions)
|
||||
}
|
||||
|
||||
+115
-96
@@ -2,184 +2,203 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package api
|
||||
package api // import "miniflux.app/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
// CreateUser is the API handler to create a new user.
|
||||
func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserPayload(request.Body())
|
||||
func (h *handler) currentUser(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.ValidateUserCreation(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
if c.store.UserExists(user.Username) {
|
||||
response.JSON().BadRequest(errors.New("This user already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.CreateUser(user)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to create this user"))
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
response.JSON().Created(user)
|
||||
json.OK(w, r, user)
|
||||
}
|
||||
|
||||
// UpdateUser is the API handler to update the given user.
|
||||
func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
var userCreationRequest model.UserCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&userCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserCreationWithPassword(h.store, &userCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.store.CreateUser(&userCreationRequest)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := decodeUserPayload(request.Body())
|
||||
json.Created(w, r, user)
|
||||
}
|
||||
|
||||
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
|
||||
var userModificationRequest model.UserModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&userModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
originalUser, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.ValidateUserModification(); err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
originalUser, err := c.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if originalUser == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
originalUser.Merge(user)
|
||||
if err = c.store.UpdateUser(originalUser); err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to update this user"))
|
||||
if !request.IsAdminUser(r) {
|
||||
if originalUser.ID != request.UserID(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
|
||||
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Created(originalUser)
|
||||
userModificationRequest.Patch(originalUser)
|
||||
if err = h.store.UpdateUser(originalUser); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.Created(w, r, originalUser)
|
||||
}
|
||||
|
||||
// Users is the API handler to get the list of users.
|
||||
func (c *Controller) Users(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
if userID != request.UserID(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := c.store.Users()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch the list of users"))
|
||||
if _, err := h.store.UserByID(userID); err != nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(users)
|
||||
if err := h.store.MarkAllAsRead(userID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
// UserByID is the API handler to fetch the given user by the ID.
|
||||
func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
func (h *handler) users(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
users, err := h.store.Users()
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.UserByID(userID)
|
||||
users.UseTimezone(request.UserTimezone(r))
|
||||
json.OK(w, r, users)
|
||||
}
|
||||
|
||||
func (h *handler) userByID(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
user, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(user)
|
||||
user.UseTimezone(request.UserTimezone(r))
|
||||
json.OK(w, r, user)
|
||||
}
|
||||
|
||||
// UserByUsername is the API handler to fetch the given user by the username.
|
||||
func (c *Controller) UserByUsername(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
func (h *handler) userByUsername(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
username := request.StringParam("username", "")
|
||||
user, err := c.store.UserByUsername(username)
|
||||
username := request.RouteStringParam(r, "username")
|
||||
user, err := h.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(user)
|
||||
json.OK(w, r, user)
|
||||
}
|
||||
|
||||
// RemoveUser is the API handler to remove an existing user.
|
||||
func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
if !ctx.IsAdminUser() {
|
||||
response.JSON().Forbidden()
|
||||
func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := request.IntegerParam("userID")
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
user, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().BadRequest(err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.UserByID(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(errors.New("Unable to fetch this user from the database"))
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
response.JSON().NotFound(errors.New("User not found"))
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.store.RemoveUser(user.ID); err != nil {
|
||||
response.JSON().BadRequest(errors.New("Unable to remove this user from the database"))
|
||||
if user.ID == request.UserID(r) {
|
||||
json.BadRequest(w, r, errors.New("You cannot remove yourself"))
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().NoContent()
|
||||
h.store.RemoveUserAsync(user.ID)
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
+13
-3
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -14,13 +14,23 @@ import (
|
||||
)
|
||||
|
||||
func askCredentials() (string, string) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
if !terminal.IsTerminal(fd) {
|
||||
fmt.Fprintf(os.Stderr, "This is not a terminal, exiting.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Print("Enter Username: ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
username, _ := reader.ReadString('\n')
|
||||
|
||||
fmt.Print("Enter Password: ")
|
||||
bytePassword, _ := terminal.ReadPassword(0)
|
||||
|
||||
state, _ := terminal.GetState(fd)
|
||||
defer terminal.Restore(fd, state)
|
||||
bytePassword, _ := terminal.ReadPassword(fd)
|
||||
|
||||
fmt.Printf("\n")
|
||||
return strings.TrimSpace(username), strings.TrimSpace(string(bytePassword))
|
||||
|
||||
+118
-25
@@ -2,63 +2,156 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/daemon"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"github.com/miniflux/miniflux/version"
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/database"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
const (
|
||||
flagInfoHelp = "Show application information"
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
flagCreateAdminHelp = "Create admin user"
|
||||
flagResetPasswordHelp = "Reset user password"
|
||||
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
||||
flagDebugModeHelp = "Show debug logs"
|
||||
flagConfigFileHelp = "Load configuration file"
|
||||
flagConfigDumpHelp = "Print parsed configuration values"
|
||||
)
|
||||
|
||||
// Parse parses command line arguments.
|
||||
func Parse() {
|
||||
flagInfo := flag.Bool("info", false, "Show application information")
|
||||
flagVersion := flag.Bool("version", false, "Show application version")
|
||||
flagMigrate := flag.Bool("migrate", false, "Migrate database schema")
|
||||
flagFlushSessions := flag.Bool("flush-sessions", false, "Flush all sessions (disconnect users)")
|
||||
flagCreateAdmin := flag.Bool("create-admin", false, "Create admin user")
|
||||
flagResetPassword := flag.Bool("reset-password", false, "Reset user password")
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
store := storage.NewStorage(
|
||||
cfg.DatabaseURL(),
|
||||
cfg.DatabaseMaxConnections(),
|
||||
var (
|
||||
err error
|
||||
flagInfo bool
|
||||
flagVersion bool
|
||||
flagMigrate bool
|
||||
flagFlushSessions bool
|
||||
flagCreateAdmin bool
|
||||
flagResetPassword bool
|
||||
flagResetFeedErrors bool
|
||||
flagDebugMode bool
|
||||
flagConfigFile string
|
||||
flagConfigDump bool
|
||||
)
|
||||
|
||||
if *flagInfo {
|
||||
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
|
||||
flag.BoolVar(&flagInfo, "i", false, flagInfoHelp)
|
||||
flag.BoolVar(&flagVersion, "version", false, flagVersionHelp)
|
||||
flag.BoolVar(&flagVersion, "v", false, flagVersionHelp)
|
||||
flag.BoolVar(&flagMigrate, "migrate", false, flagMigrateHelp)
|
||||
flag.BoolVar(&flagFlushSessions, "flush-sessions", false, flagFlushSessionsHelp)
|
||||
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
|
||||
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
|
||||
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
|
||||
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
|
||||
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
|
||||
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
|
||||
flag.BoolVar(&flagConfigDump, "config-dump", false, flagConfigDumpHelp)
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewParser()
|
||||
|
||||
if flagConfigFile != "" {
|
||||
config.Opts, err = cfg.ParseFile(flagConfigFile)
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
config.Opts, err = cfg.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
logger.Fatal("%v", err)
|
||||
}
|
||||
|
||||
if flagConfigDump {
|
||||
fmt.Print(config.Opts)
|
||||
return
|
||||
}
|
||||
|
||||
if config.Opts.LogDateTime() {
|
||||
logger.EnableDateTime()
|
||||
}
|
||||
|
||||
if flagDebugMode || config.Opts.HasDebugMode() {
|
||||
logger.EnableDebug()
|
||||
}
|
||||
|
||||
if flagInfo {
|
||||
info()
|
||||
return
|
||||
}
|
||||
|
||||
if *flagVersion {
|
||||
if flagVersion {
|
||||
fmt.Println(version.Version)
|
||||
return
|
||||
}
|
||||
|
||||
if *flagMigrate {
|
||||
store.Migrate()
|
||||
if config.Opts.IsDefaultDatabaseURL() {
|
||||
logger.Info("The default value for DATABASE_URL is used")
|
||||
}
|
||||
|
||||
db, err := database.NewConnectionPool(
|
||||
config.Opts.DatabaseURL(),
|
||||
config.Opts.DatabaseMinConns(),
|
||||
config.Opts.DatabaseMaxConns(),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatal("Unable to connect to the database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if flagMigrate {
|
||||
database.Migrate(db)
|
||||
return
|
||||
}
|
||||
|
||||
if *flagFlushSessions {
|
||||
store := storage.NewStorage(db)
|
||||
|
||||
if flagResetFeedErrors {
|
||||
store.ResetFeedErrors()
|
||||
return
|
||||
}
|
||||
|
||||
if flagFlushSessions {
|
||||
flushSessions(store)
|
||||
return
|
||||
}
|
||||
|
||||
if *flagCreateAdmin {
|
||||
if flagCreateAdmin {
|
||||
createAdmin(store)
|
||||
return
|
||||
}
|
||||
|
||||
if *flagResetPassword {
|
||||
if flagResetPassword {
|
||||
resetPassword(store)
|
||||
return
|
||||
}
|
||||
|
||||
daemon.Run(cfg, store)
|
||||
// Run migrations and start the deamon.
|
||||
if config.Opts.RunMigrations() {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
logger.Fatal(`%v`, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.IsSchemaUpToDate(db); err != nil {
|
||||
logger.Fatal(`You must run the SQL migrations, %v`, err)
|
||||
}
|
||||
|
||||
// Create admin user and start the deamon.
|
||||
if config.Opts.CreateAdmin() {
|
||||
createAdmin(store)
|
||||
}
|
||||
|
||||
startDaemon(store)
|
||||
}
|
||||
|
||||
+23
-14
@@ -2,33 +2,42 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func createAdmin(store *storage.Storage) {
|
||||
user := model.NewUser()
|
||||
user.Username = os.Getenv("ADMIN_USERNAME")
|
||||
user.Password = os.Getenv("ADMIN_PASSWORD")
|
||||
user.IsAdmin = true
|
||||
|
||||
if user.Username == "" || user.Password == "" {
|
||||
user.Username, user.Password = askCredentials()
|
||||
userCreationRequest := &model.UserCreationRequest{
|
||||
Username: config.Opts.AdminUsername(),
|
||||
Password: config.Opts.AdminPassword(),
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
if err := user.ValidateUserCreation(); err != nil {
|
||||
fmt.Println(err)
|
||||
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||
}
|
||||
|
||||
if store.UserExists(userCreationRequest.Username) {
|
||||
logger.Info(`User %q already exists, skipping creation`, userCreationRequest.Username)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := store.CreateUser(user); err != nil {
|
||||
fmt.Println(err)
|
||||
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/metric"
|
||||
"miniflux.app/service/httpd"
|
||||
"miniflux.app/service/scheduler"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/worker"
|
||||
)
|
||||
|
||||
func startDaemon(store *storage.Storage) {
|
||||
logger.Info("Starting Miniflux...")
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
signal.Notify(stop, syscall.SIGTERM)
|
||||
|
||||
pool := worker.NewPool(store, config.Opts.WorkerPoolSize())
|
||||
|
||||
if config.Opts.HasSchedulerService() && !config.Opts.HasMaintenanceMode() {
|
||||
scheduler.Serve(store, pool)
|
||||
}
|
||||
|
||||
var httpServer *http.Server
|
||||
if config.Opts.HasHTTPService() {
|
||||
httpServer = httpd.Serve(store, pool)
|
||||
}
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
collector := metric.NewCollector(store, config.Opts.MetricsRefreshInterval())
|
||||
go collector.GatherStorageMetrics()
|
||||
}
|
||||
|
||||
<-stop
|
||||
logger.Info("Shutting down the process...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if httpServer != nil {
|
||||
httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
logger.Info("Process gracefully stopped")
|
||||
}
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package cli implements command line arguments for Miniflux application.
|
||||
|
||||
*/
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"miniflux.app/storage"
|
||||
)
|
||||
|
||||
func flushSessions(store *storage.Storage) {
|
||||
fmt.Println("Flushing all sessions (disconnect users)")
|
||||
if err := store.FlushAllSessions(); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -2,17 +2,21 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/miniflux/miniflux/version"
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
func info() {
|
||||
fmt.Println("Version:", version.Version)
|
||||
fmt.Println("Commit:", version.Commit)
|
||||
fmt.Println("Build Date:", version.BuildDate)
|
||||
fmt.Println("Go Version:", runtime.Version())
|
||||
fmt.Println("Compiler:", runtime.Compiler)
|
||||
fmt.Println("Arch:", runtime.GOARCH)
|
||||
fmt.Println("OS:", runtime.GOOS)
|
||||
}
|
||||
|
||||
+12
-8
@@ -2,36 +2,40 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
package cli // import "miniflux.app/cli"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
"miniflux.app/validator"
|
||||
)
|
||||
|
||||
func resetPassword(store *storage.Storage) {
|
||||
username, password := askCredentials()
|
||||
user, err := store.UserByUsername(username)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
fmt.Println("User not found!")
|
||||
fmt.Fprintf(os.Stderr, "User not found!\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
user.Password = password
|
||||
if err := user.ValidatePassword(); err != nil {
|
||||
fmt.Println(err)
|
||||
userModificationRequest := &model.UserModificationRequest{
|
||||
Password: &password,
|
||||
}
|
||||
if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := store.UpdateUser(user); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
Miniflux API Client
|
||||
===================
|
||||
|
||||
[](https://pkg.go.dev/miniflux.app/client)
|
||||
|
||||
Client library for Miniflux REST API.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
```bash
|
||||
go get -u miniflux.app/client
|
||||
```
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
miniflux "miniflux.app/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Authentication with username/password:
|
||||
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||
|
||||
// Authentication with an API Key:
|
||||
client := miniflux.New("https://api.example.org", "my-secret-token")
|
||||
|
||||
// Fetch all feeds.
|
||||
feeds, err := client.Feeds()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(feeds)
|
||||
|
||||
// Backup your feeds to an OPML file.
|
||||
opml, err := client.Export()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile("opml.xml", opml, 0644)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,22 +1,48 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package miniflux
|
||||
package client // import "miniflux.app/client"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Client represents a Miniflux client.
|
||||
// Client holds API procedure calls.
|
||||
type Client struct {
|
||||
request *request
|
||||
}
|
||||
|
||||
// New returns a new Miniflux client.
|
||||
func New(endpoint string, credentials ...string) *Client {
|
||||
if len(credentials) == 2 {
|
||||
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
||||
}
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
}
|
||||
|
||||
// Me returns the logged user information.
|
||||
func (c *Client) Me() (*User, error) {
|
||||
body, err := c.request.Get("/v1/me")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var user *User
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: json error (%v)", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Users returns all users.
|
||||
func (c *Client) Users() (Users, error) {
|
||||
body, err := c.request.Get("/v1/users")
|
||||
@@ -70,7 +96,11 @@ func (c *Client) UserByUsername(username string) (*User, error) {
|
||||
|
||||
// CreateUser creates a new user in the system.
|
||||
func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
|
||||
body, err := c.request.Post("/v1/users", &User{Username: username, Password: password, IsAdmin: isAdmin})
|
||||
body, err := c.request.Post("/v1/users", &UserCreationRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
IsAdmin: isAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,8 +116,8 @@ func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, err
|
||||
}
|
||||
|
||||
// UpdateUser updates a user in the system.
|
||||
func (c *Client) UpdateUser(user *User) (*User, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", user.ID), user)
|
||||
func (c *Client) UpdateUser(userID int64, userChanges *UserModificationRequest) (*User, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", userID), userChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,12 +134,13 @@ func (c *Client) UpdateUser(user *User) (*User, error) {
|
||||
|
||||
// DeleteUser removes a user from the system.
|
||||
func (c *Client) DeleteUser(userID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all unread entries as read for a given user.
|
||||
func (c *Client) MarkAllAsRead(userID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Discover try to find subscriptions from a website.
|
||||
@@ -186,15 +217,32 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// DeleteCategory removes a category.
|
||||
func (c *Client) DeleteCategory(categoryID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
|
||||
// MarkCategoryAsRead marks all unread entries in a category as read.
|
||||
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CategoryFeeds gets feeds of a cateogry.
|
||||
func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/feeds", categoryID))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
return nil
|
||||
var feeds Feeds
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&feeds); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return feeds, nil
|
||||
}
|
||||
|
||||
// DeleteCategory removes a category.
|
||||
func (c *Client) DeleteCategory(categoryID int64) error {
|
||||
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
|
||||
}
|
||||
|
||||
// Feeds gets all feeds.
|
||||
@@ -230,6 +278,12 @@ func (c *Client) Export() ([]byte, error) {
|
||||
return opml, nil
|
||||
}
|
||||
|
||||
// Import imports an OPML file.
|
||||
func (c *Client) Import(f io.ReadCloser) error {
|
||||
_, err := c.request.PostFile("/v1/import", f)
|
||||
return err
|
||||
}
|
||||
|
||||
// Feed gets a feed.
|
||||
func (c *Client) Feed(feedID int64) (*Feed, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID))
|
||||
@@ -248,11 +302,8 @@ func (c *Client) Feed(feedID int64) (*Feed, error) {
|
||||
}
|
||||
|
||||
// CreateFeed creates a new feed.
|
||||
func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
|
||||
body, err := c.request.Post("/v1/feeds", map[string]interface{}{
|
||||
"feed_url": url,
|
||||
"category_id": categoryID,
|
||||
})
|
||||
func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) (int64, error) {
|
||||
body, err := c.request.Post("/v1/feeds", feedCreationRequest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -272,40 +323,42 @@ func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
|
||||
}
|
||||
|
||||
// UpdateFeed updates a feed.
|
||||
func (c *Client) UpdateFeed(feed *Feed) (*Feed, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feed.ID), feed)
|
||||
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feedID), feedChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var f *Feed
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&f); err != nil {
|
||||
if err := json.NewDecoder(body).Decode(&f); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// RefreshFeed refresh a feed.
|
||||
// MarkFeedAsRead marks all unread entries of the feed as read.
|
||||
func (c *Client) MarkFeedAsRead(feedID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshAllFeeds refreshes all feeds.
|
||||
func (c *Client) RefreshAllFeeds() error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/refresh"), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshFeed refreshes a feed.
|
||||
func (c *Client) RefreshFeed(feedID int64) error {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFeed removes a feed.
|
||||
func (c *Client) DeleteFeed(feedID int64) error {
|
||||
body, err := c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
return nil
|
||||
return c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
|
||||
}
|
||||
|
||||
// FeedIcon gets a feed icon.
|
||||
@@ -342,6 +395,23 @@ func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// CategoryEntry gets a single category entry.
|
||||
func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var entry *Entry
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&entry); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Entry gets a single entry.
|
||||
func (c *Client) Entry(entryID int64) (*Entry, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID))
|
||||
@@ -397,6 +467,25 @@ func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, err
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CategoryEntries fetch entries of a category.
|
||||
func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) {
|
||||
path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter)
|
||||
|
||||
body, err := c.request.Get(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var result EntryResultSet
|
||||
decoder := json.NewDecoder(body)
|
||||
if err := decoder.Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateEntries updates the status of a list of entries.
|
||||
func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||
type payload struct {
|
||||
@@ -404,29 +493,14 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
body, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
|
||||
return nil
|
||||
_, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
|
||||
return err
|
||||
}
|
||||
|
||||
// ToggleBookmark toggles entry bookmark value.
|
||||
func (c *Client) ToggleBookmark(entryID int64) error {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClient returns a new Client.
|
||||
func NewClient(endpoint, username, password string) *Client {
|
||||
return &Client{request: &request{endpoint: endpoint, username: username, password: password}}
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildFilterQueryString(path string, filter *Filter) string {
|
||||
@@ -453,6 +527,42 @@ func buildFilterQueryString(path string, filter *Filter) string {
|
||||
values.Set("offset", strconv.Itoa(filter.Offset))
|
||||
}
|
||||
|
||||
if filter.After > 0 {
|
||||
values.Set("after", strconv.FormatInt(filter.After, 10))
|
||||
}
|
||||
|
||||
if filter.AfterEntryID > 0 {
|
||||
values.Set("after_entry_id", strconv.FormatInt(filter.AfterEntryID, 10))
|
||||
}
|
||||
|
||||
if filter.Before > 0 {
|
||||
values.Set("before", strconv.FormatInt(filter.Before, 10))
|
||||
}
|
||||
|
||||
if filter.BeforeEntryID > 0 {
|
||||
values.Set("before_entry_id", strconv.FormatInt(filter.BeforeEntryID, 10))
|
||||
}
|
||||
|
||||
if filter.Starred {
|
||||
values.Set("starred", "1")
|
||||
}
|
||||
|
||||
if filter.Search != "" {
|
||||
values.Set("search", filter.Search)
|
||||
}
|
||||
|
||||
if filter.CategoryID > 0 {
|
||||
values.Set("category_id", strconv.FormatInt(filter.CategoryID, 10))
|
||||
}
|
||||
|
||||
if filter.FeedID > 0 {
|
||||
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
|
||||
}
|
||||
|
||||
for _, status := range filter.Statuses {
|
||||
values.Add("status", status)
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("%s?%s", path, values.Encode())
|
||||
}
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/client"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry statuses.
|
||||
const (
|
||||
EntryStatusUnread = "unread"
|
||||
EntryStatusRead = "read"
|
||||
EntryStatusRemoved = "removed"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
|
||||
}
|
||||
|
||||
// UserCreationRequest represents the request to create a user.
|
||||
type UserCreationRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
}
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []User
|
||||
|
||||
// Category represents a feed category.
|
||||
type Category struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c Category) String() string {
|
||||
return fmt.Sprintf("#%d %s", c.ID, c.Title)
|
||||
}
|
||||
|
||||
// Categories represents a list of categories.
|
||||
type Categories []*Category
|
||||
|
||||
// Subscription represents a feed subscription.
|
||||
type Subscription struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (s Subscription) String() string {
|
||||
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
|
||||
}
|
||||
|
||||
// Subscriptions represents a list of subscriptions.
|
||||
type Subscriptions []*Subscription
|
||||
|
||||
// Feed represents a Miniflux feed.
|
||||
type Feed struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at,omitempty"`
|
||||
EtagHeader string `json:"etag_header,omitempty"`
|
||||
LastModifiedHeader string `json:"last_modified_header,omitempty"`
|
||||
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
|
||||
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
type FeedCreationRequest struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Crawler bool `json:"crawler"`
|
||||
Disabled bool `json:"disabled"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
type FeedModificationRequest struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
Disabled *bool `json:"disabled"`
|
||||
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
type FeedIcon struct {
|
||||
ID int64 `json:"id"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Feeds represents a list of feeds.
|
||||
type Feeds []*Feed
|
||||
|
||||
// Entry represents a subscription item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// Enclosures represents a list of attachments.
|
||||
type Enclosures []*Enclosure
|
||||
|
||||
// Filter is used to filter entries.
|
||||
type Filter struct {
|
||||
Status string
|
||||
Offset int
|
||||
Limit int
|
||||
Order string
|
||||
Direction string
|
||||
Starred bool
|
||||
Before int64
|
||||
After int64
|
||||
BeforeEntryID int64
|
||||
AfterEntryID int64
|
||||
Search string
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
type EntryResultSet struct {
|
||||
Total int `json:"total"`
|
||||
Entries Entries `json:"entries"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package client implements a client library for the Miniflux REST API.
|
||||
|
||||
Examples
|
||||
|
||||
This code snippet fetch the list of users:
|
||||
|
||||
import (
|
||||
miniflux "miniflux.app/client"
|
||||
)
|
||||
|
||||
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||
users, err := client.Users()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(users, err)
|
||||
|
||||
This one discover subscriptions on a website:
|
||||
|
||||
subscriptions, err := client.Discover("https://example.org/")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(subscriptions)
|
||||
|
||||
*/
|
||||
package client // import "miniflux.app/client"
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package miniflux
|
||||
package client // import "miniflux.app/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -18,14 +18,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "Miniflux Client Library <https://github.com/miniflux/miniflux-go>"
|
||||
userAgent = "Miniflux Client Library"
|
||||
defaultTimeout = 80
|
||||
)
|
||||
|
||||
// List of exposed errors.
|
||||
var (
|
||||
errNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
|
||||
errForbidden = errors.New("miniflux: access forbidden")
|
||||
errServerError = errors.New("miniflux: internal server error")
|
||||
ErrNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
|
||||
ErrForbidden = errors.New("miniflux: access forbidden")
|
||||
ErrServerError = errors.New("miniflux: internal server error")
|
||||
ErrNotFound = errors.New("miniflux: resource not found")
|
||||
)
|
||||
|
||||
type errorResponse struct {
|
||||
@@ -36,6 +38,7 @@ type request struct {
|
||||
endpoint string
|
||||
username string
|
||||
password string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (r *request) Get(path string) (io.ReadCloser, error) {
|
||||
@@ -46,12 +49,17 @@ func (r *request) Post(path string, data interface{}) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodPost, path, data)
|
||||
}
|
||||
|
||||
func (r *request) PostFile(path string, f io.ReadCloser) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodPost, path, f)
|
||||
}
|
||||
|
||||
func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodPut, path, data)
|
||||
}
|
||||
|
||||
func (r *request) Delete(path string) (io.ReadCloser, error) {
|
||||
return r.execute(http.MethodDelete, path, nil)
|
||||
func (r *request) Delete(path string) error {
|
||||
_, err := r.execute(http.MethodDelete, path, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
|
||||
@@ -69,10 +77,18 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
Method: method,
|
||||
Header: r.buildHeaders(),
|
||||
}
|
||||
request.SetBasicAuth(r.username, r.password)
|
||||
|
||||
if r.username != "" && r.password != "" {
|
||||
request.SetBasicAuth(r.username, r.password)
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
|
||||
switch data.(type) {
|
||||
case io.ReadCloser:
|
||||
request.Body = data.(io.ReadCloser)
|
||||
default:
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
|
||||
}
|
||||
}
|
||||
|
||||
client := r.buildClient()
|
||||
@@ -83,11 +99,20 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return nil, errNotAuthorized
|
||||
response.Body.Close()
|
||||
return nil, ErrNotAuthorized
|
||||
case http.StatusForbidden:
|
||||
return nil, errForbidden
|
||||
response.Body.Close()
|
||||
return nil, ErrForbidden
|
||||
case http.StatusInternalServerError:
|
||||
return nil, errServerError
|
||||
response.Body.Close()
|
||||
return nil, ErrServerError
|
||||
case http.StatusNotFound:
|
||||
response.Body.Close()
|
||||
return nil, ErrNotFound
|
||||
case http.StatusNoContent:
|
||||
response.Body.Close()
|
||||
return nil, nil
|
||||
case http.StatusBadRequest:
|
||||
defer response.Body.Close()
|
||||
|
||||
@@ -100,8 +125,9 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("miniflux: server error (statusCode=%d)", response.StatusCode)
|
||||
if response.StatusCode > 400 {
|
||||
response.Body.Close()
|
||||
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return response.Body, nil
|
||||
@@ -118,6 +144,9 @@ func (r *request) buildHeaders() http.Header {
|
||||
headers.Add("User-Agent", userAgent)
|
||||
headers.Add("Content-Type", "application/json")
|
||||
headers.Add("Accept", "application/json")
|
||||
if r.apiKey != "" {
|
||||
headers.Add("X-Auth-Token", r.apiKey)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
+4
-135
@@ -1,139 +1,8 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultDatabaseURL = "postgres://postgres:postgres@localhost/miniflux2?sslmode=disable"
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 10
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCertCache = "/tmp/cert_cache"
|
||||
defaultSessionCleanupFrequency = 24
|
||||
)
|
||||
|
||||
// Config manages configuration parameters.
|
||||
type Config struct {
|
||||
IsHTTPS bool
|
||||
}
|
||||
|
||||
func (c *Config) get(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *Config) getInt(key string, fallback int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
v, _ := strconv.Atoi(value)
|
||||
return v
|
||||
}
|
||||
|
||||
// BaseURL returns the application base URL.
|
||||
func (c *Config) BaseURL() string {
|
||||
return c.get("BASE_URL", defaultBaseURL)
|
||||
}
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (c *Config) DatabaseURL() string {
|
||||
return c.get("DATABASE_URL", defaultDatabaseURL)
|
||||
}
|
||||
|
||||
// DatabaseMaxConnections returns the number of maximum database connections.
|
||||
func (c *Config) DatabaseMaxConnections() int {
|
||||
return c.getInt("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
|
||||
}
|
||||
|
||||
// ListenAddr returns the listen address for the HTTP server.
|
||||
func (c *Config) ListenAddr() string {
|
||||
return c.get("LISTEN_ADDR", defaultListenAddr)
|
||||
}
|
||||
|
||||
// CertFile returns the SSL certificate filename if any.
|
||||
func (c *Config) CertFile() string {
|
||||
return c.get("CERT_FILE", defaultCertFile)
|
||||
}
|
||||
|
||||
// KeyFile returns the private key filename for custom SSL certificate.
|
||||
func (c *Config) KeyFile() string {
|
||||
return c.get("KEY_FILE", defaultKeyFile)
|
||||
}
|
||||
|
||||
// CertDomain returns the domain to use for Let's Encrypt certificate.
|
||||
func (c *Config) CertDomain() string {
|
||||
return c.get("CERT_DOMAIN", defaultCertDomain)
|
||||
}
|
||||
|
||||
// CertCache returns the directory to use for Let's Encrypt session cache.
|
||||
func (c *Config) CertCache() string {
|
||||
return c.get("CERT_CACHE", defaultCertCache)
|
||||
}
|
||||
|
||||
// SessionCleanupFrequency returns the interval for session cleanup.
|
||||
func (c *Config) SessionCleanupFrequency() int {
|
||||
return c.getInt("SESSION_CLEANUP_FREQUENCY", defaultSessionCleanupFrequency)
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
func (c *Config) WorkerPoolSize() int {
|
||||
return c.getInt("WORKER_POOL_SIZE", defaultWorkerPoolSize)
|
||||
}
|
||||
|
||||
// PollingFrequency returns the interval to refresh feeds in the background.
|
||||
func (c *Config) PollingFrequency() int {
|
||||
return c.getInt("POLLING_FREQUENCY", defaultPollingFrequency)
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (c *Config) BatchSize() int {
|
||||
return c.getInt("BATCH_SIZE", defaultBatchSize)
|
||||
}
|
||||
|
||||
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
|
||||
func (c *Config) IsOAuth2UserCreationAllowed() bool {
|
||||
return c.getInt("OAUTH2_USER_CREATION", 0) == 1
|
||||
}
|
||||
|
||||
// OAuth2ClientID returns the OAuth2 Client ID.
|
||||
func (c *Config) OAuth2ClientID() string {
|
||||
return c.get("OAUTH2_CLIENT_ID", "")
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret returns the OAuth2 client secret.
|
||||
func (c *Config) OAuth2ClientSecret() string {
|
||||
return c.get("OAUTH2_CLIENT_SECRET", "")
|
||||
}
|
||||
|
||||
// OAuth2RedirectURL returns the OAuth2 redirect URL.
|
||||
func (c *Config) OAuth2RedirectURL() string {
|
||||
return c.get("OAUTH2_REDIRECT_URL", "")
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (c *Config) OAuth2Provider() string {
|
||||
return c.get("OAUTH2_PROVIDER", "")
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config.
|
||||
func NewConfig() *Config {
|
||||
return &Config{IsHTTPS: os.Getenv("HTTPS") != ""}
|
||||
}
|
||||
// Opts holds parsed configuration options.
|
||||
var Opts *Options
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package config handles configuration values for Miniflux application.
|
||||
Package config handles configuration management for the application.
|
||||
|
||||
*/
|
||||
package config
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPS = false
|
||||
defaultLogDateTime = false
|
||||
defaultHSTS = true
|
||||
defaultHTTPService = true
|
||||
defaultSchedulerService = true
|
||||
defaultDebug = false
|
||||
defaultTiming = false
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultRootURL = "http://localhost"
|
||||
defaultBasePath = ""
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultPollingFrequency = 60
|
||||
defaultBatchSize = 10
|
||||
defaultPollingScheduler = "round_robin"
|
||||
defaultSchedulerEntryFrequencyMinInterval = 5
|
||||
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
|
||||
defaultPollingParsingErrorLimit = 3
|
||||
defaultRunMigrations = false
|
||||
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
defaultDatabaseMaxConns = 20
|
||||
defaultDatabaseMinConns = 1
|
||||
defaultListenAddr = "127.0.0.1:8080"
|
||||
defaultCertFile = ""
|
||||
defaultKeyFile = ""
|
||||
defaultCertDomain = ""
|
||||
defaultCleanupFrequencyHours = 24
|
||||
defaultCleanupArchiveReadDays = 60
|
||||
defaultCleanupArchiveUnreadDays = 180
|
||||
defaultCleanupRemoveSessionsDays = 30
|
||||
defaultProxyImages = "http-only"
|
||||
defaultCreateAdmin = false
|
||||
defaultAdminUsername = ""
|
||||
defaultAdminPassword = ""
|
||||
defaultOAuth2UserCreation = false
|
||||
defaultOAuth2ClientID = ""
|
||||
defaultOAuth2ClientSecret = ""
|
||||
defaultOAuth2RedirectURL = ""
|
||||
defaultOAuth2OidcDiscoveryEndpoint = ""
|
||||
defaultOAuth2Provider = ""
|
||||
defaultPocketConsumerKey = ""
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15
|
||||
defaultHTTPClientProxy = ""
|
||||
defaultAuthProxyHeader = ""
|
||||
defaultAuthProxyUserCreation = false
|
||||
defaultMaintenanceMode = false
|
||||
defaultMaintenanceMessage = "Miniflux is currently under maintenance"
|
||||
defaultMetricsCollector = false
|
||||
defaultMetricsRefreshInterval = 60
|
||||
defaultMetricsAllowedNetworks = "127.0.0.1/8"
|
||||
)
|
||||
|
||||
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
|
||||
// Option contains a key to value map of a single option. It may be used to output debug strings.
|
||||
type Option struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Options contains configuration options.
|
||||
type Options struct {
|
||||
HTTPS bool
|
||||
logDateTime bool
|
||||
hsts bool
|
||||
httpService bool
|
||||
schedulerService bool
|
||||
debug bool
|
||||
serverTimingHeader bool
|
||||
baseURL string
|
||||
rootURL string
|
||||
basePath string
|
||||
databaseURL string
|
||||
databaseMaxConns int
|
||||
databaseMinConns int
|
||||
runMigrations bool
|
||||
listenAddr string
|
||||
certFile string
|
||||
certDomain string
|
||||
certKeyFile string
|
||||
cleanupFrequencyHours int
|
||||
cleanupArchiveReadDays int
|
||||
cleanupArchiveUnreadDays int
|
||||
cleanupRemoveSessionsDays int
|
||||
pollingFrequency int
|
||||
batchSize int
|
||||
pollingScheduler string
|
||||
schedulerEntryFrequencyMinInterval int
|
||||
schedulerEntryFrequencyMaxInterval int
|
||||
pollingParsingErrorLimit int
|
||||
workerPoolSize int
|
||||
createAdmin bool
|
||||
adminUsername string
|
||||
adminPassword string
|
||||
proxyImages string
|
||||
oauth2UserCreationAllowed bool
|
||||
oauth2ClientID string
|
||||
oauth2ClientSecret string
|
||||
oauth2RedirectURL string
|
||||
oauth2OidcDiscoveryEndpoint string
|
||||
oauth2Provider string
|
||||
pocketConsumerKey string
|
||||
httpClientTimeout int
|
||||
httpClientMaxBodySize int64
|
||||
httpClientProxy string
|
||||
httpClientUserAgent string
|
||||
authProxyHeader string
|
||||
authProxyUserCreation bool
|
||||
maintenanceMode bool
|
||||
maintenanceMessage string
|
||||
metricsCollector bool
|
||||
metricsRefreshInterval int
|
||||
metricsAllowedNetworks []string
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
HTTPS: defaultHTTPS,
|
||||
logDateTime: defaultLogDateTime,
|
||||
hsts: defaultHSTS,
|
||||
httpService: defaultHTTPService,
|
||||
schedulerService: defaultSchedulerService,
|
||||
debug: defaultDebug,
|
||||
serverTimingHeader: defaultTiming,
|
||||
baseURL: defaultBaseURL,
|
||||
rootURL: defaultRootURL,
|
||||
basePath: defaultBasePath,
|
||||
databaseURL: defaultDatabaseURL,
|
||||
databaseMaxConns: defaultDatabaseMaxConns,
|
||||
databaseMinConns: defaultDatabaseMinConns,
|
||||
runMigrations: defaultRunMigrations,
|
||||
listenAddr: defaultListenAddr,
|
||||
certFile: defaultCertFile,
|
||||
certDomain: defaultCertDomain,
|
||||
certKeyFile: defaultKeyFile,
|
||||
cleanupFrequencyHours: defaultCleanupFrequencyHours,
|
||||
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
|
||||
cleanupArchiveUnreadDays: defaultCleanupArchiveUnreadDays,
|
||||
cleanupRemoveSessionsDays: defaultCleanupRemoveSessionsDays,
|
||||
pollingFrequency: defaultPollingFrequency,
|
||||
batchSize: defaultBatchSize,
|
||||
pollingScheduler: defaultPollingScheduler,
|
||||
schedulerEntryFrequencyMinInterval: defaultSchedulerEntryFrequencyMinInterval,
|
||||
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
|
||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||
workerPoolSize: defaultWorkerPoolSize,
|
||||
createAdmin: defaultCreateAdmin,
|
||||
proxyImages: defaultProxyImages,
|
||||
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
|
||||
oauth2ClientID: defaultOAuth2ClientID,
|
||||
oauth2ClientSecret: defaultOAuth2ClientSecret,
|
||||
oauth2RedirectURL: defaultOAuth2RedirectURL,
|
||||
oauth2OidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
|
||||
oauth2Provider: defaultOAuth2Provider,
|
||||
pocketConsumerKey: defaultPocketConsumerKey,
|
||||
httpClientTimeout: defaultHTTPClientTimeout,
|
||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||
httpClientProxy: defaultHTTPClientProxy,
|
||||
httpClientUserAgent: defaultHTTPClientUserAgent,
|
||||
authProxyHeader: defaultAuthProxyHeader,
|
||||
authProxyUserCreation: defaultAuthProxyUserCreation,
|
||||
maintenanceMode: defaultMaintenanceMode,
|
||||
maintenanceMessage: defaultMaintenanceMessage,
|
||||
metricsCollector: defaultMetricsCollector,
|
||||
metricsRefreshInterval: defaultMetricsRefreshInterval,
|
||||
metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks},
|
||||
}
|
||||
}
|
||||
|
||||
// LogDateTime returns true if the date/time should be displayed in log messages.
|
||||
func (o *Options) LogDateTime() bool {
|
||||
return o.logDateTime
|
||||
}
|
||||
|
||||
// HasMaintenanceMode returns true if maintenance mode is enabled.
|
||||
func (o *Options) HasMaintenanceMode() bool {
|
||||
return o.maintenanceMode
|
||||
}
|
||||
|
||||
// MaintenanceMessage returns maintenance message.
|
||||
func (o *Options) MaintenanceMessage() string {
|
||||
return o.maintenanceMessage
|
||||
}
|
||||
|
||||
// HasDebugMode returns true if debug mode is enabled.
|
||||
func (o *Options) HasDebugMode() bool {
|
||||
return o.debug
|
||||
}
|
||||
|
||||
// HasServerTimingHeader returns true if server-timing headers enabled.
|
||||
func (o *Options) HasServerTimingHeader() bool {
|
||||
return o.serverTimingHeader
|
||||
}
|
||||
|
||||
// BaseURL returns the application base URL with path.
|
||||
func (o *Options) BaseURL() string {
|
||||
return o.baseURL
|
||||
}
|
||||
|
||||
// RootURL returns the base URL without path.
|
||||
func (o *Options) RootURL() string {
|
||||
return o.rootURL
|
||||
}
|
||||
|
||||
// BasePath returns the application base path according to the base URL.
|
||||
func (o *Options) BasePath() string {
|
||||
return o.basePath
|
||||
}
|
||||
|
||||
// IsDefaultDatabaseURL returns true if the default database URL is used.
|
||||
func (o *Options) IsDefaultDatabaseURL() bool {
|
||||
return o.databaseURL == defaultDatabaseURL
|
||||
}
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (o *Options) DatabaseURL() string {
|
||||
return o.databaseURL
|
||||
}
|
||||
|
||||
// DatabaseMaxConns returns the maximum number of database connections.
|
||||
func (o *Options) DatabaseMaxConns() int {
|
||||
return o.databaseMaxConns
|
||||
}
|
||||
|
||||
// DatabaseMinConns returns the minimum number of database connections.
|
||||
func (o *Options) DatabaseMinConns() int {
|
||||
return o.databaseMinConns
|
||||
}
|
||||
|
||||
// ListenAddr returns the listen address for the HTTP server.
|
||||
func (o *Options) ListenAddr() string {
|
||||
return o.listenAddr
|
||||
}
|
||||
|
||||
// CertFile returns the SSL certificate filename if any.
|
||||
func (o *Options) CertFile() string {
|
||||
return o.certFile
|
||||
}
|
||||
|
||||
// CertKeyFile returns the private key filename for custom SSL certificate.
|
||||
func (o *Options) CertKeyFile() string {
|
||||
return o.certKeyFile
|
||||
}
|
||||
|
||||
// CertDomain returns the domain to use for Let's Encrypt certificate.
|
||||
func (o *Options) CertDomain() string {
|
||||
return o.certDomain
|
||||
}
|
||||
|
||||
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
|
||||
func (o *Options) CleanupFrequencyHours() int {
|
||||
return o.cleanupFrequencyHours
|
||||
}
|
||||
|
||||
// CleanupArchiveReadDays returns the number of days after which marking read items as removed.
|
||||
func (o *Options) CleanupArchiveReadDays() int {
|
||||
return o.cleanupArchiveReadDays
|
||||
}
|
||||
|
||||
// CleanupArchiveUnreadDays returns the number of days after which marking unread items as removed.
|
||||
func (o *Options) CleanupArchiveUnreadDays() int {
|
||||
return o.cleanupArchiveUnreadDays
|
||||
}
|
||||
|
||||
// CleanupRemoveSessionsDays returns the number of days after which to remove sessions.
|
||||
func (o *Options) CleanupRemoveSessionsDays() int {
|
||||
return o.cleanupRemoveSessionsDays
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
func (o *Options) WorkerPoolSize() int {
|
||||
return o.workerPoolSize
|
||||
}
|
||||
|
||||
// PollingFrequency returns the interval to refresh feeds in the background.
|
||||
func (o *Options) PollingFrequency() int {
|
||||
return o.pollingFrequency
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (o *Options) BatchSize() int {
|
||||
return o.batchSize
|
||||
}
|
||||
|
||||
// PollingScheduler returns the scheduler used for polling feeds.
|
||||
func (o *Options) PollingScheduler() string {
|
||||
return o.pollingScheduler
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMaxInterval returns the maximum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMaxInterval() int {
|
||||
return o.schedulerEntryFrequencyMaxInterval
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMinInterval returns the minimum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMinInterval() int {
|
||||
return o.schedulerEntryFrequencyMinInterval
|
||||
}
|
||||
|
||||
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
|
||||
func (o *Options) PollingParsingErrorLimit() int {
|
||||
return o.pollingParsingErrorLimit
|
||||
}
|
||||
|
||||
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
|
||||
func (o *Options) IsOAuth2UserCreationAllowed() bool {
|
||||
return o.oauth2UserCreationAllowed
|
||||
}
|
||||
|
||||
// OAuth2ClientID returns the OAuth2 Client ID.
|
||||
func (o *Options) OAuth2ClientID() string {
|
||||
return o.oauth2ClientID
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret returns the OAuth2 client secret.
|
||||
func (o *Options) OAuth2ClientSecret() string {
|
||||
return o.oauth2ClientSecret
|
||||
}
|
||||
|
||||
// OAuth2RedirectURL returns the OAuth2 redirect URL.
|
||||
func (o *Options) OAuth2RedirectURL() string {
|
||||
return o.oauth2RedirectURL
|
||||
}
|
||||
|
||||
// OAuth2OidcDiscoveryEndpoint returns the OAuth2 OIDC discovery endpoint.
|
||||
func (o *Options) OAuth2OidcDiscoveryEndpoint() string {
|
||||
return o.oauth2OidcDiscoveryEndpoint
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (o *Options) OAuth2Provider() string {
|
||||
return o.oauth2Provider
|
||||
}
|
||||
|
||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||
func (o *Options) HasHSTS() bool {
|
||||
return o.hsts
|
||||
}
|
||||
|
||||
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
|
||||
func (o *Options) RunMigrations() bool {
|
||||
return o.runMigrations
|
||||
}
|
||||
|
||||
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
|
||||
func (o *Options) CreateAdmin() bool {
|
||||
return o.createAdmin
|
||||
}
|
||||
|
||||
// AdminUsername returns the admin username if defined.
|
||||
func (o *Options) AdminUsername() string {
|
||||
return o.adminUsername
|
||||
}
|
||||
|
||||
// AdminPassword returns the admin password if defined.
|
||||
func (o *Options) AdminPassword() string {
|
||||
return o.adminPassword
|
||||
}
|
||||
|
||||
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) ProxyImages() string {
|
||||
return o.proxyImages
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
func (o *Options) HasHTTPService() bool {
|
||||
return o.httpService
|
||||
}
|
||||
|
||||
// HasSchedulerService returns true if the scheduler service is enabled.
|
||||
func (o *Options) HasSchedulerService() bool {
|
||||
return o.schedulerService
|
||||
}
|
||||
|
||||
// PocketConsumerKey returns the Pocket Consumer Key if configured.
|
||||
func (o *Options) PocketConsumerKey(defaultValue string) string {
|
||||
if o.pocketConsumerKey != "" {
|
||||
return o.pocketConsumerKey
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// HTTPClientTimeout returns the time limit in seconds before the HTTP client cancel the request.
|
||||
func (o *Options) HTTPClientTimeout() int {
|
||||
return o.httpClientTimeout
|
||||
}
|
||||
|
||||
// HTTPClientMaxBodySize returns the number of bytes allowed for the HTTP client to transfer.
|
||||
func (o *Options) HTTPClientMaxBodySize() int64 {
|
||||
return o.httpClientMaxBodySize
|
||||
}
|
||||
|
||||
// HTTPClientProxy returns the proxy URL for HTTP client.
|
||||
func (o *Options) HTTPClientProxy() string {
|
||||
return o.httpClientProxy
|
||||
}
|
||||
|
||||
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
|
||||
func (o *Options) HasHTTPClientProxyConfigured() bool {
|
||||
return o.httpClientProxy != ""
|
||||
}
|
||||
|
||||
// AuthProxyHeader returns an HTTP header name that contains username for
|
||||
// authentication using auth proxy.
|
||||
func (o *Options) AuthProxyHeader() string {
|
||||
return o.authProxyHeader
|
||||
}
|
||||
|
||||
// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for
|
||||
// users authenticated using auth proxy.
|
||||
func (o *Options) IsAuthProxyUserCreationAllowed() bool {
|
||||
return o.authProxyUserCreation
|
||||
}
|
||||
|
||||
// HasMetricsCollector returns true if metrics collection is enabled.
|
||||
func (o *Options) HasMetricsCollector() bool {
|
||||
return o.metricsCollector
|
||||
}
|
||||
|
||||
// MetricsRefreshInterval returns the refresh interval in seconds.
|
||||
func (o *Options) MetricsRefreshInterval() int {
|
||||
return o.metricsRefreshInterval
|
||||
}
|
||||
|
||||
// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint.
|
||||
func (o *Options) MetricsAllowedNetworks() []string {
|
||||
return o.metricsAllowedNetworks
|
||||
}
|
||||
|
||||
// HTTPClientUserAgent returns the global User-Agent header for miniflux.
|
||||
func (o *Options) HTTPClientUserAgent() string {
|
||||
return o.httpClientUserAgent
|
||||
}
|
||||
|
||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||
func (o *Options) SortedOptions() []*Option {
|
||||
var keyValues = map[string]interface{}{
|
||||
"ADMIN_PASSWORD": o.adminPassword,
|
||||
"ADMIN_USERNAME": o.adminUsername,
|
||||
"AUTH_PROXY_HEADER": o.authProxyHeader,
|
||||
"AUTH_PROXY_USER_CREATION": o.authProxyUserCreation,
|
||||
"BASE_PATH": o.basePath,
|
||||
"BASE_URL": o.baseURL,
|
||||
"BATCH_SIZE": o.batchSize,
|
||||
"CERT_DOMAIN": o.certDomain,
|
||||
"CERT_FILE": o.certFile,
|
||||
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
|
||||
"CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays,
|
||||
"CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours,
|
||||
"CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays,
|
||||
"CREATE_ADMIN": o.createAdmin,
|
||||
"DATABASE_MAX_CONNS": o.databaseMaxConns,
|
||||
"DATABASE_MIN_CONNS": o.databaseMinConns,
|
||||
"DATABASE_URL": o.databaseURL,
|
||||
"DEBUG": o.debug,
|
||||
"HSTS": o.hsts,
|
||||
"HTTPS": o.HTTPS,
|
||||
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
||||
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
||||
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
|
||||
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
|
||||
"HTTP_SERVICE": o.httpService,
|
||||
"KEY_FILE": o.certKeyFile,
|
||||
"LISTEN_ADDR": o.listenAddr,
|
||||
"LOG_DATE_TIME": o.logDateTime,
|
||||
"MAINTENANCE_MESSAGE": o.maintenanceMessage,
|
||||
"MAINTENANCE_MODE": o.maintenanceMode,
|
||||
"METRICS_ALLOWED_NETWORKS": o.metricsAllowedNetworks,
|
||||
"METRICS_COLLECTOR": o.metricsCollector,
|
||||
"METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval,
|
||||
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
|
||||
"OAUTH2_CLIENT_SECRET": o.oauth2ClientSecret,
|
||||
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint,
|
||||
"OAUTH2_PROVIDER": o.oauth2Provider,
|
||||
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
|
||||
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
||||
"POCKET_CONSUMER_KEY": o.pocketConsumerKey,
|
||||
"POLLING_FREQUENCY": o.pollingFrequency,
|
||||
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
||||
"POLLING_SCHEDULER": o.pollingScheduler,
|
||||
"PROXY_IMAGES": o.proxyImages,
|
||||
"ROOT_URL": o.rootURL,
|
||||
"RUN_MIGRATIONS": o.runMigrations,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
|
||||
"SCHEDULER_SERVICE": o.schedulerService,
|
||||
"SERVER_TIMING_HEADER": o.serverTimingHeader,
|
||||
"WORKER_POOL_SIZE": o.workerPoolSize,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keyValues))
|
||||
for key := range keyValues {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var sortedOptions []*Option
|
||||
for _, key := range keys {
|
||||
sortedOptions = append(sortedOptions, &Option{Key: key, Value: keyValues[key]})
|
||||
}
|
||||
return sortedOptions
|
||||
}
|
||||
|
||||
func (o *Options) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
for _, option := range o.SortedOptions() {
|
||||
builder.WriteString(fmt.Sprintf("%s: %v\n", option.Key, option.Value))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
url_parser "net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parser handles configuration parsing.
|
||||
type Parser struct {
|
||||
opts *Options
|
||||
}
|
||||
|
||||
// NewParser returns a new Parser.
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
opts: NewOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseEnvironmentVariables loads configuration values from environment variables.
|
||||
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
|
||||
err := p.parseLines(os.Environ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.opts, nil
|
||||
}
|
||||
|
||||
// ParseFile loads configuration values from a local file.
|
||||
func (p *Parser) ParseFile(filename string) (*Options, error) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
err = p.parseLines(p.parseFileContent(fp))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.opts, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (p *Parser) parseLines(lines []string) (err error) {
|
||||
var port string
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
key := strings.TrimSpace(fields[0])
|
||||
value := strings.TrimSpace(fields[1])
|
||||
|
||||
switch key {
|
||||
case "LOG_DATE_TIME":
|
||||
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
|
||||
case "DEBUG":
|
||||
p.opts.debug = parseBool(value, defaultDebug)
|
||||
case "SERVER_TIMING_HEADER":
|
||||
p.opts.serverTimingHeader = parseBool(value, defaultTiming)
|
||||
case "BASE_URL":
|
||||
p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "PORT":
|
||||
port = value
|
||||
case "LISTEN_ADDR":
|
||||
p.opts.listenAddr = parseString(value, defaultListenAddr)
|
||||
case "DATABASE_URL":
|
||||
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
|
||||
case "DATABASE_URL_FILE":
|
||||
p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
|
||||
case "DATABASE_MAX_CONNS":
|
||||
p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
|
||||
case "DATABASE_MIN_CONNS":
|
||||
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
||||
case "RUN_MIGRATIONS":
|
||||
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
||||
case "DISABLE_HSTS":
|
||||
p.opts.hsts = !parseBool(value, defaultHSTS)
|
||||
case "HTTPS":
|
||||
p.opts.HTTPS = parseBool(value, defaultHTTPS)
|
||||
case "DISABLE_SCHEDULER_SERVICE":
|
||||
p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
|
||||
case "DISABLE_HTTP_SERVICE":
|
||||
p.opts.httpService = !parseBool(value, defaultHTTPService)
|
||||
case "CERT_FILE":
|
||||
p.opts.certFile = parseString(value, defaultCertFile)
|
||||
case "KEY_FILE":
|
||||
p.opts.certKeyFile = parseString(value, defaultKeyFile)
|
||||
case "CERT_DOMAIN":
|
||||
p.opts.certDomain = parseString(value, defaultCertDomain)
|
||||
case "CLEANUP_FREQUENCY_HOURS":
|
||||
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
|
||||
case "CLEANUP_ARCHIVE_READ_DAYS":
|
||||
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
|
||||
case "CLEANUP_ARCHIVE_UNREAD_DAYS":
|
||||
p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
|
||||
case "CLEANUP_REMOVE_SESSIONS_DAYS":
|
||||
p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
|
||||
case "WORKER_POOL_SIZE":
|
||||
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
|
||||
case "POLLING_FREQUENCY":
|
||||
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
|
||||
case "BATCH_SIZE":
|
||||
p.opts.batchSize = parseInt(value, defaultBatchSize)
|
||||
case "POLLING_SCHEDULER":
|
||||
p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
|
||||
case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
|
||||
p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval)
|
||||
case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
|
||||
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
|
||||
case "POLLING_PARSING_ERROR_LIMIT":
|
||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||
case "PROXY_IMAGES":
|
||||
p.opts.proxyImages = parseString(value, defaultProxyImages)
|
||||
case "CREATE_ADMIN":
|
||||
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
||||
case "ADMIN_USERNAME":
|
||||
p.opts.adminUsername = parseString(value, defaultAdminUsername)
|
||||
case "ADMIN_USERNAME_FILE":
|
||||
p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
|
||||
case "ADMIN_PASSWORD":
|
||||
p.opts.adminPassword = parseString(value, defaultAdminPassword)
|
||||
case "ADMIN_PASSWORD_FILE":
|
||||
p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
|
||||
case "POCKET_CONSUMER_KEY":
|
||||
p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
|
||||
case "POCKET_CONSUMER_KEY_FILE":
|
||||
p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
|
||||
case "OAUTH2_USER_CREATION":
|
||||
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
|
||||
case "OAUTH2_CLIENT_ID":
|
||||
p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
|
||||
case "OAUTH2_CLIENT_ID_FILE":
|
||||
p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
|
||||
case "OAUTH2_CLIENT_SECRET":
|
||||
p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
|
||||
case "OAUTH2_CLIENT_SECRET_FILE":
|
||||
p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
|
||||
case "OAUTH2_REDIRECT_URL":
|
||||
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
||||
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
||||
p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
||||
case "OAUTH2_PROVIDER":
|
||||
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
||||
case "HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
||||
case "HTTP_CLIENT_PROXY":
|
||||
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
|
||||
case "HTTP_CLIENT_USER_AGENT":
|
||||
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
|
||||
case "AUTH_PROXY_HEADER":
|
||||
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
|
||||
case "AUTH_PROXY_USER_CREATION":
|
||||
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
|
||||
case "MAINTENANCE_MODE":
|
||||
p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
|
||||
case "MAINTENANCE_MESSAGE":
|
||||
p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
|
||||
case "METRICS_COLLECTOR":
|
||||
p.opts.metricsCollector = parseBool(value, defaultMetricsCollector)
|
||||
case "METRICS_REFRESH_INTERVAL":
|
||||
p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval)
|
||||
case "METRICS_ALLOWED_NETWORKS":
|
||||
p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
|
||||
}
|
||||
}
|
||||
|
||||
if port != "" {
|
||||
p.opts.listenAddr = ":" + port
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBaseURL(value string) (string, string, string, error) {
|
||||
if value == "" {
|
||||
return defaultBaseURL, defaultRootURL, "", nil
|
||||
}
|
||||
|
||||
if value[len(value)-1:] == "/" {
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
|
||||
url, err := url_parser.Parse(value)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(url.Scheme)
|
||||
if scheme != "https" && scheme != "http" {
|
||||
return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
|
||||
}
|
||||
|
||||
basePath := url.Path
|
||||
url.Path = ""
|
||||
return value, url.String(), basePath, nil
|
||||
}
|
||||
|
||||
func parseBool(value string, fallback bool) bool {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
value = strings.ToLower(value)
|
||||
if value == "1" || value == "yes" || value == "true" || value == "on" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseInt(value string, fallback int) int {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func parseString(value string, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseStringList(value string, fallback []string) []string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
var strList []string
|
||||
items := strings.Split(value, ",")
|
||||
for _, item := range items {
|
||||
strList = append(strList, strings.TrimSpace(item))
|
||||
}
|
||||
|
||||
return strList
|
||||
}
|
||||
|
||||
func readSecretFile(filename, fallback string) string {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
value := string(bytes.TrimSpace(data))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config // import "miniflux.app/config"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseBoolValue(t *testing.T) {
|
||||
scenarios := map[string]bool{
|
||||
"": true,
|
||||
"1": true,
|
||||
"Yes": true,
|
||||
"yes": true,
|
||||
"True": true,
|
||||
"true": true,
|
||||
"on": true,
|
||||
"false": false,
|
||||
"off": false,
|
||||
"invalid": false,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
result := parseBool(input, true)
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringValueWithUnsetVariable(t *testing.T) {
|
||||
if parseString("", "defaultValue") != "defaultValue" {
|
||||
t.Errorf(`Unset variables should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringValue(t *testing.T) {
|
||||
if parseString("test", "defaultValue") != "test" {
|
||||
t.Errorf(`Defined variables should returns the specified value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValueWithUnsetVariable(t *testing.T) {
|
||||
if parseInt("", 42) != 42 {
|
||||
t.Errorf(`Unset variables should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValueWithInvalidInput(t *testing.T) {
|
||||
if parseInt("invalid integer", 42) != 42 {
|
||||
t.Errorf(`Invalid integer should returns the default value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIntValue(t *testing.T) {
|
||||
if parseInt("2018", 42) != 2018 {
|
||||
t.Errorf(`Defined variables should returns the specified value`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
The contrib directory contains various useful things contributed by the community.
|
||||
|
||||
Community contributions are not officially supported by the maintainers.
|
||||
There is no guarantee whatsoever that anything in this folder works.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
miniflux_linux_user: miniflux
|
||||
miniflux_db_user_name: miniflux_db_user
|
||||
miniflux_db_user_password: miniflux_db_user_password
|
||||
miniflux_db: miniflux_db
|
||||
miniflux_admin_name: admin
|
||||
miniflux_admin_passwort: miniflux_admin_password
|
||||
miniflux_port: 8080
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
- hosts: miniflux
|
||||
roles:
|
||||
- { role: mgrote.miniflux, tags: "miniflux" }
|
||||
@@ -0,0 +1,23 @@
|
||||
## mgrote.miniflux
|
||||
|
||||
### Details
|
||||
Installs and configures Miniflux v2 with ansible
|
||||
|
||||
### Works on...
|
||||
- [x] Ubuntu (>=18.04)
|
||||
|
||||
### Variables and Defaults
|
||||
##### Linux User
|
||||
miniflux_linux_user: miniflux
|
||||
##### DB User
|
||||
miniflux_db_user_name: miniflux_db_user
|
||||
##### DB Password
|
||||
miniflux_db_user_password: qqqqqqqqqqqqq
|
||||
##### Database
|
||||
miniflux_db: miniflux_db
|
||||
##### Username Miniflux Admin
|
||||
miniflux_admin_name: admin
|
||||
##### Password Miniflux Admin
|
||||
miniflux_admin_passwort: hallowelt
|
||||
##### Port for Miniflux Frontend
|
||||
miniflux_port: 8080
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: start_miniflux.service
|
||||
become: yes
|
||||
systemd:
|
||||
name: miniflux
|
||||
state: restarted
|
||||
enabled: yes
|
||||
# wait 15 seconds(for systemd)
|
||||
- name: miniflux_wait
|
||||
wait_for:
|
||||
timeout: 15
|
||||
@@ -0,0 +1,43 @@
|
||||
- name: add Apt-key for miniflux-repo
|
||||
become: yes
|
||||
apt_key:
|
||||
url: https://apt.miniflux.app/KEY.gpg
|
||||
state: present
|
||||
|
||||
- name: add miniflux-repo
|
||||
become: yes
|
||||
apt_repository:
|
||||
repo: 'deb https://apt.miniflux.app/ /'
|
||||
state: present
|
||||
filename: miniflux_repo
|
||||
update_cache: yes
|
||||
|
||||
- name: install miniflux
|
||||
become: yes
|
||||
apt:
|
||||
name: miniflux
|
||||
state: present
|
||||
|
||||
- name: add miniflux linux_user
|
||||
become: yes
|
||||
user:
|
||||
name: "{{ miniflux_linux_user }}"
|
||||
home: "/var/empty"
|
||||
create_home: "no"
|
||||
system: "yes"
|
||||
shell: "/bin/false"
|
||||
|
||||
- name: create directory "/etc/miniflux.d"
|
||||
become: yes
|
||||
file:
|
||||
path: /etc/miniflux.d
|
||||
state: directory
|
||||
|
||||
- name: copy miniflux.conf
|
||||
become: yes
|
||||
template:
|
||||
src: "miniflux.conf"
|
||||
dest: "/etc/miniflux.conf"
|
||||
notify:
|
||||
- start_miniflux.service
|
||||
- miniflux_wait
|
||||
@@ -0,0 +1,18 @@
|
||||
# See https://docs.miniflux.app/
|
||||
|
||||
LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
|
||||
DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
|
||||
|
||||
POLLING_FREQUENCY=15
|
||||
PROXY_IMAGES=http-only
|
||||
|
||||
# Run SQL migrations automatically:
|
||||
RUN_MIGRATIONS=1
|
||||
|
||||
CREATE_ADMIN=1
|
||||
ADMIN_USERNAME={{ miniflux_admin_name }}
|
||||
ADMIN_PASSWORD={{ miniflux_admin_passwort }}
|
||||
|
||||
POLLING_FREQUENCY=10
|
||||
|
||||
# Options: https://miniflux.app/miniflux.1.html
|
||||
@@ -0,0 +1,2 @@
|
||||
miniflux.example.org
|
||||
reverse_proxy miniflux:8080
|
||||
@@ -0,0 +1,13 @@
|
||||
Docker-Compose Examples
|
||||
=======================
|
||||
|
||||
Here are few Docker Compose examples:
|
||||
|
||||
- `basic.yml`: Basic example
|
||||
- `caddy.yml`: Use Caddy as reverse-proxy with automatic HTTPS
|
||||
- `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
|
||||
|
||||
```bash
|
||||
docker-compose -f basic.yml up -d db
|
||||
docker-compose -f basic.yml up
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
ports:
|
||||
- "80:8080"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -0,0 +1,38 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
container_name: caddy
|
||||
depends_on:
|
||||
- miniflux
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- $PWD/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
- BASE_URL=https://miniflux.example.org
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
volumes:
|
||||
miniflux-db:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,48 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.3"
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
|
||||
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
|
||||
depends_on:
|
||||
- miniflux
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "./letsencrypt:/letsencrypt"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
miniflux:
|
||||
image: miniflux/miniflux:latest
|
||||
container_name: miniflux
|
||||
depends_on:
|
||||
- db
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
|
||||
- RUN_MIGRATIONS=1
|
||||
- CREATE_ADMIN=1
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
- BASE_URL=https://miniflux.example.org
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.miniflux.rule=Host(`miniflux.example.org`)"
|
||||
- "traefik.http.routers.miniflux.entrypoints=websecure"
|
||||
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
volumes:
|
||||
miniflux-db:
|
||||
@@ -0,0 +1 @@
|
||||
Grafana Dashboard for Miniflux
|
||||
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -2,12 +2,13 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
package crypto // import "miniflux.app/crypto"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte {
|
||||
func GenerateRandomString(size int) string {
|
||||
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
|
||||
}
|
||||
|
||||
// GenerateRandomStringHex returns a random hexadecimal string.
|
||||
func GenerateRandomStringHex(size int) string {
|
||||
return hex.EncodeToString(GenerateRandomBytes(size))
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package crypto implements helpers related to cryptography.
|
||||
|
||||
*/
|
||||
package crypto
|
||||
package crypto // import "miniflux.app/crypto"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
// Run starts the daemon.
|
||||
func Run(cfg *config.Config, store *storage.Storage) {
|
||||
logger.Info("Starting Miniflux...")
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
signal.Notify(stop, syscall.SIGTERM)
|
||||
|
||||
feedHandler := feed.NewFeedHandler(store)
|
||||
pool := scheduler.NewWorkerPool(feedHandler, cfg.WorkerPoolSize())
|
||||
server := newServer(cfg, store, pool, feedHandler)
|
||||
|
||||
scheduler.NewFeedScheduler(
|
||||
store,
|
||||
pool,
|
||||
cfg.PollingFrequency(),
|
||||
cfg.BatchSize(),
|
||||
)
|
||||
|
||||
scheduler.NewSessionScheduler(store, cfg.SessionCleanupFrequency())
|
||||
|
||||
<-stop
|
||||
logger.Info("Shutting down the server...")
|
||||
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
server.Shutdown(ctx)
|
||||
store.Close()
|
||||
logger.Info("Server gracefully stopped")
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package daemon handles the main application process.
|
||||
|
||||
*/
|
||||
package daemon
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/miniflux/miniflux/api"
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/fever"
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/http/middleware"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/reader/opml"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
"github.com/miniflux/miniflux/template"
|
||||
"github.com/miniflux/miniflux/ui"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
translator := locale.Load()
|
||||
templateEngine := template.NewEngine(cfg, router, translator)
|
||||
|
||||
apiController := api.NewController(store, feedHandler)
|
||||
feverController := fever.NewController(store)
|
||||
uiController := ui.NewController(cfg, store, pool, feedHandler, opml.NewHandler(store))
|
||||
|
||||
apiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewBasicAuthMiddleware(store).Handler,
|
||||
))
|
||||
|
||||
feverHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewFeverMiddleware(store).Handler,
|
||||
))
|
||||
|
||||
uiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
|
||||
middleware.NewUserSessionMiddleware(store, router).Handler,
|
||||
middleware.NewSessionMiddleware(cfg, store).Handler,
|
||||
))
|
||||
|
||||
router.Handle("/fever/", feverHandler.Use(feverController.Handler))
|
||||
|
||||
router.Handle("/v1/users", apiHandler.Use(apiController.CreateUser)).Methods("POST")
|
||||
router.Handle("/v1/users", apiHandler.Use(apiController.Users)).Methods("GET")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT")
|
||||
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE")
|
||||
router.Handle("/v1/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET")
|
||||
|
||||
router.Handle("/v1/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST")
|
||||
router.Handle("/v1/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET")
|
||||
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT")
|
||||
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE")
|
||||
|
||||
router.Handle("/v1/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST")
|
||||
|
||||
router.Handle("/v1/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST")
|
||||
router.Handle("/v1/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get")
|
||||
router.Handle("/v1/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT")
|
||||
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE")
|
||||
router.Handle("/v1/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
|
||||
router.Handle("/v1/export", apiHandler.Use(apiController.Export)).Methods("GET")
|
||||
|
||||
router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
|
||||
router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
|
||||
router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
|
||||
router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
|
||||
router.Handle("/v1/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
|
||||
router.Handle("/v1/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
|
||||
|
||||
router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
|
||||
router.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
|
||||
router.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET")
|
||||
router.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET")
|
||||
router.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET")
|
||||
|
||||
router.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET")
|
||||
router.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST")
|
||||
router.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST")
|
||||
|
||||
router.Handle("/mark-all-as-read", uiHandler.Use(uiController.MarkAllAsRead)).Name("markAllAsRead").Methods("GET")
|
||||
|
||||
router.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
|
||||
router.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
|
||||
router.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
|
||||
|
||||
router.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST")
|
||||
router.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST")
|
||||
router.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET")
|
||||
router.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET")
|
||||
router.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET")
|
||||
|
||||
router.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET")
|
||||
router.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET")
|
||||
router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
|
||||
router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
|
||||
router.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
|
||||
|
||||
router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
|
||||
router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
|
||||
router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
|
||||
router.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
|
||||
|
||||
router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
|
||||
router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
|
||||
router.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST")
|
||||
router.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET")
|
||||
router.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST")
|
||||
router.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST")
|
||||
|
||||
router.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET")
|
||||
router.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET")
|
||||
|
||||
router.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET")
|
||||
router.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET")
|
||||
router.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST")
|
||||
router.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET")
|
||||
router.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST")
|
||||
router.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST")
|
||||
|
||||
router.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET")
|
||||
|
||||
router.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET")
|
||||
router.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST")
|
||||
|
||||
router.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET")
|
||||
router.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET")
|
||||
router.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST")
|
||||
|
||||
router.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET")
|
||||
router.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST")
|
||||
|
||||
router.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET")
|
||||
router.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
|
||||
router.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
|
||||
|
||||
router.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET")
|
||||
router.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
|
||||
router.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
|
||||
|
||||
router.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
|
||||
router.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
|
||||
router.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
|
||||
|
||||
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("User-agent: *\nDisallow: /"))
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *http.Server {
|
||||
certFile := cfg.CertFile()
|
||||
keyFile := cfg.KeyFile()
|
||||
certDomain := cfg.CertDomain()
|
||||
certCache := cfg.CertCache()
|
||||
server := &http.Server{
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
Addr: cfg.ListenAddr(),
|
||||
Handler: routes(cfg, store, feedHandler, pool),
|
||||
}
|
||||
|
||||
if certDomain != "" && certCache != "" {
|
||||
cfg.IsHTTPS = true
|
||||
server.Addr = ":https"
|
||||
certManager := autocert.Manager{
|
||||
Cache: autocert.DirCache(certCache),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(certDomain),
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info(`Listening on "%s" by using auto-configured certificate for "%s"`, server.Addr, certDomain)
|
||||
logger.Fatal(server.Serve(certManager.Listener()).Error())
|
||||
}()
|
||||
} else if certFile != "" && keyFile != "" {
|
||||
server.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
cfg.IsHTTPS = true
|
||||
|
||||
go func() {
|
||||
logger.Info(`Listening on "%s" by using certificate "%s" and key "%s"`, server.Addr, certFile, keyFile)
|
||||
logger.Fatal(server.ListenAndServeTLS(certFile, keyFile).Error())
|
||||
}()
|
||||
} else {
|
||||
go func() {
|
||||
logger.Info(`Listening on "%s" without TLS`, server.Addr)
|
||||
logger.Fatal(server.ListenAndServe().Error())
|
||||
}()
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package database // import "miniflux.app/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
// Postgresql driver import
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// NewConnectionPool configures the database connection pool.
|
||||
func NewConnectionPool(dsn string, minConnections, maxConnections int) (*sql.DB, error) {
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(maxConnections)
|
||||
db.SetMaxIdleConns(minConnections)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
fmt.Println("-> Current schema version:", currentVersion)
|
||||
fmt.Println("-> Latest schema version:", schemaVersion)
|
||||
|
||||
for version := currentVersion; version < schemaVersion; version++ {
|
||||
newVersion := version + 1
|
||||
fmt.Println("* Migrating to version:", newVersion)
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if err := migrations[version](tx); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSchemaUpToDate checks if the database schema is up to date.
|
||||
func IsSchemaUpToDate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
if currentVersion < schemaVersion {
|
||||
return fmt.Errorf(`the database schema is not up to date: current=v%d expected=v%d`, currentVersion, schemaVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
// Copyright 2020 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package database // import "miniflux.app/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
var schemaVersion = len(migrations)
|
||||
|
||||
var migrations = []func(tx *sql.Tx) error{
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE schema_version (
|
||||
version text not null
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id serial not null,
|
||||
username text not null unique,
|
||||
password text,
|
||||
is_admin bool default 'f',
|
||||
language text default 'en_US',
|
||||
timezone text default 'UTC',
|
||||
theme text default 'default',
|
||||
last_login_at timestamp with time zone,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
token text not null unique,
|
||||
created_at timestamp with time zone default now(),
|
||||
user_agent text,
|
||||
ip text,
|
||||
primary key (id),
|
||||
unique (user_id, token),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE categories (
|
||||
id serial not null,
|
||||
user_id int not null,
|
||||
title text not null,
|
||||
primary key (id),
|
||||
unique (user_id, title),
|
||||
foreign key (user_id) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE feeds (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
category_id int not null,
|
||||
title text not null,
|
||||
feed_url text not null,
|
||||
site_url text not null,
|
||||
checked_at timestamp with time zone default now(),
|
||||
etag_header text default '',
|
||||
last_modified_header text default '',
|
||||
parsing_error_msg text default '',
|
||||
parsing_error_count int default 0,
|
||||
primary key (id),
|
||||
unique (user_id, feed_url),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (category_id) references categories(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TYPE entry_status as enum('unread', 'read', 'removed');
|
||||
|
||||
CREATE TABLE entries (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
feed_id bigint not null,
|
||||
hash text not null,
|
||||
published_at timestamp with time zone not null,
|
||||
title text not null,
|
||||
url text not null,
|
||||
author text,
|
||||
content text,
|
||||
status entry_status default 'unread',
|
||||
primary key (id),
|
||||
unique (feed_id, hash),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (feed_id) references feeds(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE INDEX entries_feed_idx on entries using btree(feed_id);
|
||||
|
||||
CREATE TABLE enclosures (
|
||||
id bigserial not null,
|
||||
user_id int not null,
|
||||
entry_id bigint not null,
|
||||
url text not null,
|
||||
size int default 0,
|
||||
mime_type text default '',
|
||||
primary key (id),
|
||||
foreign key (user_id) references users(id) on delete cascade,
|
||||
foreign key (entry_id) references entries(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE icons (
|
||||
id bigserial not null,
|
||||
hash text not null unique,
|
||||
mime_type text not null,
|
||||
content bytea not null,
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
CREATE TABLE feed_icons (
|
||||
feed_id bigint not null,
|
||||
icon_id bigint not null,
|
||||
primary key(feed_id, icon_id),
|
||||
foreign key (feed_id) references feeds(id) on delete cascade,
|
||||
foreign key (icon_id) references icons(id) on delete cascade
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE EXTENSION IF NOT EXISTS hstore;
|
||||
ALTER TABLE users ADD COLUMN extra hstore;
|
||||
CREATE INDEX users_extra_idx ON users using gin(extra);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE tokens (
|
||||
id text not null,
|
||||
value text not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id, value)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
|
||||
ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE integrations (
|
||||
user_id int not null,
|
||||
pinboard_enabled bool default 'f',
|
||||
pinboard_token text default '',
|
||||
pinboard_tags text default 'miniflux',
|
||||
pinboard_mark_as_unread bool default 'f',
|
||||
instapaper_enabled bool default 'f',
|
||||
instapaper_username text default '',
|
||||
instapaper_password text default '',
|
||||
fever_enabled bool default 'f',
|
||||
fever_username text default '',
|
||||
fever_password text default '',
|
||||
fever_token text default '',
|
||||
primary key(user_id)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE sessions rename to user_sessions`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
DROP TABLE tokens;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id text not null,
|
||||
data jsonb not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
primary key(id)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_client_id text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_client_secret text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_username text default '';
|
||||
ALTER TABLE integrations ADD COLUMN wallabag_password text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
|
||||
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN nunux_keeper_api_key text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN pocket_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN pocket_access_token text default '';
|
||||
ALTER TABLE integrations ADD COLUMN pocket_consumer_key text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds ADD COLUMN username text default '';
|
||||
ALTER TABLE feeds ADD COLUMN password text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
|
||||
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
||||
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE
|
||||
entries
|
||||
SET
|
||||
document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
|
||||
UPDATE users SET theme='light_serif' WHERE theme='default';
|
||||
UPDATE users SET theme='light_sans_serif' WHERE theme='sansserif';
|
||||
UPDATE users SET theme='dark_serif' WHERE theme='black';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
|
||||
UPDATE entries SET changed_at = published_at;
|
||||
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE api_keys (
|
||||
id serial not null,
|
||||
user_id int not null references users(id) on delete cascade,
|
||||
token text not null unique,
|
||||
description text not null,
|
||||
last_used_at timestamp with time zone,
|
||||
created_at timestamp with time zone default now(),
|
||||
primary key(id),
|
||||
unique (user_id, description)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN share_code text not null default '';
|
||||
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
|
||||
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE integrations DROP COLUMN fever_password`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds
|
||||
ADD COLUMN blocklist_rules text not null default '',
|
||||
ADD COLUMN keeplist_rules text not null default ''
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
|
||||
UPDATE entries SET created_at = published_at;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE users
|
||||
ADD column stylesheet text not null default '',
|
||||
ADD column google_id text not null default '',
|
||||
ADD column openid_connect_id text not null default ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
DECLARE my_cursor CURSOR FOR
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(extra->'custom_css', '') as custom_css,
|
||||
COALESCE(extra->'google_id', '') as google_id,
|
||||
COALESCE(extra->'oidc_id', '') as oidc_id
|
||||
FROM users
|
||||
FOR UPDATE
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Exec("CLOSE my_cursor")
|
||||
|
||||
for {
|
||||
var (
|
||||
userID int64
|
||||
customStylesheet string
|
||||
googleID string
|
||||
oidcID string
|
||||
)
|
||||
|
||||
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := tx.Exec(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
stylesheet=$2,
|
||||
google_id=$3,
|
||||
openid_connect_id=$4
|
||||
WHERE
|
||||
id=$1
|
||||
`,
|
||||
userID, customStylesheet, googleID, oidcID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE users DROP COLUMN extra;
|
||||
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
|
||||
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
|
||||
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE acme_cache (
|
||||
key varchar(400) not null primary key,
|
||||
data bytea not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package main implements a feed reader application.
|
||||
Miniflux is a feed reader application.
|
||||
|
||||
*/
|
||||
package main
|
||||
package main // import "miniflux.app"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Hervé GOUCHET
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package duration implements helpers to calculate time duration.
|
||||
|
||||
*/
|
||||
package duration
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2017 Hervé Gouchet. All rights reserved.
|
||||
// Use of this source code is governed by the MIT License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package duration
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
)
|
||||
|
||||
// Texts to be translated if necessary.
|
||||
var (
|
||||
NotYet = `not yet`
|
||||
JustNow = `just now`
|
||||
LastMinute = `1 minute ago`
|
||||
Minutes = `%d minutes ago`
|
||||
LastHour = `1 hour ago`
|
||||
Hours = `%d hours ago`
|
||||
Yesterday = `yesterday`
|
||||
Days = `%d days ago`
|
||||
Weeks = `%d weeks ago`
|
||||
Months = `%d months ago`
|
||||
Years = `%d years ago`
|
||||
)
|
||||
|
||||
// ElapsedTime returns in a human readable format the elapsed time
|
||||
// since the given datetime.
|
||||
func ElapsedTime(translator *locale.Language, t time.Time) string {
|
||||
if t.IsZero() || time.Now().Before(t) {
|
||||
return translator.Get(NotYet)
|
||||
}
|
||||
diff := time.Since(t)
|
||||
// Duration in seconds
|
||||
s := diff.Seconds()
|
||||
// Duration in days
|
||||
d := int(s / 86400)
|
||||
switch {
|
||||
case s < 60:
|
||||
return translator.Get(JustNow)
|
||||
case s < 120:
|
||||
return translator.Get(LastMinute)
|
||||
case s < 3600:
|
||||
return translator.Get(Minutes, int(diff.Minutes()))
|
||||
case s < 7200:
|
||||
return translator.Get(LastHour)
|
||||
case s < 86400:
|
||||
return translator.Get(Hours, int(diff.Hours()))
|
||||
case d == 1:
|
||||
return translator.Get(Yesterday)
|
||||
case d < 7:
|
||||
return translator.Get(Days, d)
|
||||
case d < 31:
|
||||
return translator.Get(Weeks, int(math.Ceil(float64(d)/7)))
|
||||
case d < 365:
|
||||
return translator.Get(Months, int(math.Ceil(float64(d)/30)))
|
||||
default:
|
||||
return translator.Get(Years, int(math.Ceil(float64(d)/365)))
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2017 Hervé Gouchet. All rights reserved.
|
||||
// Use of this source code is governed by the MIT License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package duration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
)
|
||||
|
||||
func TestElapsedTime(t *testing.T) {
|
||||
var dt = []struct {
|
||||
in time.Time
|
||||
out string
|
||||
}{
|
||||
{time.Time{}, NotYet},
|
||||
{time.Now().Add(time.Hour), NotYet},
|
||||
{time.Now(), JustNow},
|
||||
{time.Now().Add(-time.Minute), LastMinute},
|
||||
{time.Now().Add(-time.Minute * 40), fmt.Sprintf(Minutes, 40)},
|
||||
{time.Now().Add(-time.Hour), LastHour},
|
||||
{time.Now().Add(-time.Hour * 3), fmt.Sprintf(Hours, 3)},
|
||||
{time.Now().Add(-time.Hour * 32), Yesterday},
|
||||
{time.Now().Add(-time.Hour * 24 * 3), fmt.Sprintf(Days, 3)},
|
||||
{time.Now().Add(-time.Hour * 24 * 14), fmt.Sprintf(Weeks, 2)},
|
||||
{time.Now().Add(-time.Hour * 24 * 60), fmt.Sprintf(Months, 2)},
|
||||
{time.Now().Add(-time.Hour * 24 * 365 * 3), fmt.Sprintf(Years, 3)},
|
||||
}
|
||||
for i, tt := range dt {
|
||||
if out := ElapsedTime(&locale.Language{}, tt.in); out != tt.out {
|
||||
t.Errorf("%d. content mismatch for %v:exp=%q got=%q", i, tt.in, tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package errors handles localized errors.
|
||||
|
||||
*/
|
||||
package errors
|
||||
package errors // import "miniflux.app/errors"
|
||||
|
||||
+6
-6
@@ -2,12 +2,12 @@
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package errors
|
||||
package errors // import "miniflux.app/errors"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"miniflux.app/locale"
|
||||
)
|
||||
|
||||
// LocalizedError represents an error than could be translated to another language.
|
||||
@@ -22,11 +22,11 @@ func (l LocalizedError) Error() string {
|
||||
}
|
||||
|
||||
// Localize returns the translated error message.
|
||||
func (l LocalizedError) Localize(translation *locale.Language) string {
|
||||
return translation.Get(l.message, l.args...)
|
||||
func (l LocalizedError) Localize(printer *locale.Printer) string {
|
||||
return printer.Printf(l.message, l.args...)
|
||||
}
|
||||
|
||||
// NewLocalizedError returns a new LocalizedError.
|
||||
func NewLocalizedError(message string, args ...interface{}) LocalizedError {
|
||||
return LocalizedError{message: message, args: args}
|
||||
func NewLocalizedError(message string, args ...interface{}) *LocalizedError {
|
||||
return &LocalizedError{message: message, args: args}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package fever implements Fever API endpoints.
|
||||
|
||||
*/
|
||||
package fever
|
||||
package fever // import "miniflux.app/fever"
|
||||
|
||||
-631
@@ -1,631 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fever
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/http/handler"
|
||||
"github.com/miniflux/miniflux/integration"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/storage"
|
||||
)
|
||||
|
||||
type baseResponse struct {
|
||||
Version int `json:"api_version"`
|
||||
Authenticated int `json:"auth"`
|
||||
LastRefresh int64 `json:"last_refreshed_on_time"`
|
||||
}
|
||||
|
||||
func (b *baseResponse) SetCommonValues() {
|
||||
b.Version = 3
|
||||
b.Authenticated = 1
|
||||
b.LastRefresh = time.Now().Unix()
|
||||
}
|
||||
|
||||
/*
|
||||
The default response is a JSON object containing two members:
|
||||
|
||||
api_version contains the version of the API responding (positive integer)
|
||||
auth whether the request was successfully authenticated (boolean integer)
|
||||
|
||||
The API can also return XML by passing xml as the optional value of the api argument like so:
|
||||
|
||||
http://yourdomain.com/fever/?api=xml
|
||||
|
||||
The top level XML element is named response.
|
||||
|
||||
The response to each successfully authenticated request will have auth set to 1 and include
|
||||
at least one additional member:
|
||||
|
||||
last_refreshed_on_time contains the time of the most recently refreshed (not updated)
|
||||
feed (Unix timestamp/integer)
|
||||
|
||||
*/
|
||||
func newBaseResponse() baseResponse {
|
||||
r := baseResponse{}
|
||||
r.SetCommonValues()
|
||||
return r
|
||||
}
|
||||
|
||||
type groupsResponse struct {
|
||||
baseResponse
|
||||
Groups []group `json:"groups"`
|
||||
FeedsGroups []feedsGroups `json:"feeds_groups"`
|
||||
}
|
||||
|
||||
type feedsResponse struct {
|
||||
baseResponse
|
||||
Feeds []feed `json:"feeds"`
|
||||
FeedsGroups []feedsGroups `json:"feeds_groups"`
|
||||
}
|
||||
|
||||
type faviconsResponse struct {
|
||||
baseResponse
|
||||
Favicons []favicon `json:"favicons"`
|
||||
}
|
||||
|
||||
type itemsResponse struct {
|
||||
baseResponse
|
||||
Items []item `json:"items"`
|
||||
Total int `json:"total_items"`
|
||||
}
|
||||
|
||||
type unreadResponse struct {
|
||||
baseResponse
|
||||
ItemIDs string `json:"unread_item_ids"`
|
||||
}
|
||||
|
||||
type savedResponse struct {
|
||||
baseResponse
|
||||
ItemIDs string `json:"saved_item_ids"`
|
||||
}
|
||||
|
||||
type group struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type feedsGroups struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
FeedIDs string `json:"feed_ids"`
|
||||
}
|
||||
|
||||
type feed struct {
|
||||
ID int64 `json:"id"`
|
||||
FaviconID int64 `json:"favicon_id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
IsSpark int `json:"is_spark"`
|
||||
LastUpdated int64 `json:"last_updated_on_time"`
|
||||
}
|
||||
|
||||
type item struct {
|
||||
ID int64 `json:"id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
HTML string `json:"html"`
|
||||
URL string `json:"url"`
|
||||
IsSaved int `json:"is_saved"`
|
||||
IsRead int `json:"is_read"`
|
||||
CreatedAt int64 `json:"created_on_time"`
|
||||
}
|
||||
|
||||
type favicon struct {
|
||||
ID int64 `json:"id"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Controller implements the Fever API.
|
||||
type Controller struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
// Handler handles Fever API calls
|
||||
func (c *Controller) Handler(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
switch {
|
||||
case request.HasQueryParam("groups"):
|
||||
c.handleGroups(ctx, request, response)
|
||||
case request.HasQueryParam("feeds"):
|
||||
c.handleFeeds(ctx, request, response)
|
||||
case request.HasQueryParam("favicons"):
|
||||
c.handleFavicons(ctx, request, response)
|
||||
case request.HasQueryParam("unread_item_ids"):
|
||||
c.handleUnreadItems(ctx, request, response)
|
||||
case request.HasQueryParam("saved_item_ids"):
|
||||
c.handleSavedItems(ctx, request, response)
|
||||
case request.HasQueryParam("items"):
|
||||
c.handleItems(ctx, request, response)
|
||||
case request.FormValue("mark") == "item":
|
||||
c.handleWriteItems(ctx, request, response)
|
||||
case request.FormValue("mark") == "feed":
|
||||
c.handleWriteFeeds(ctx, request, response)
|
||||
case request.FormValue("mark") == "group":
|
||||
c.handleWriteGroups(ctx, request, response)
|
||||
default:
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the groups argument will return two additional members:
|
||||
|
||||
groups contains an array of group objects
|
||||
feeds_groups contains an array of feeds_group objects
|
||||
|
||||
A group object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
title (utf-8 string)
|
||||
|
||||
The feeds_group object is documented under “Feeds/Groups Relationships.”
|
||||
|
||||
The “Kindling” super group is not included in this response and is composed of all feeds with
|
||||
an is_spark equal to 0.
|
||||
|
||||
The “Sparks” super group is not included in this response and is composed of all feeds with an
|
||||
is_spark equal to 1.
|
||||
|
||||
*/
|
||||
func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
|
||||
|
||||
categories, err := c.store.Categories(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := c.store.Feeds(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var result groupsResponse
|
||||
for _, category := range categories {
|
||||
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
|
||||
}
|
||||
|
||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the feeds argument will return two additional members:
|
||||
|
||||
feeds contains an array of group objects
|
||||
feeds_groups contains an array of feeds_group objects
|
||||
|
||||
A feed object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
favicon_id (positive integer)
|
||||
title (utf-8 string)
|
||||
url (utf-8 string)
|
||||
site_url (utf-8 string)
|
||||
is_spark (boolean integer)
|
||||
last_updated_on_time (Unix timestamp/integer)
|
||||
|
||||
The feeds_group object is documented under “Feeds/Groups Relationships.”
|
||||
|
||||
The “All Items” super feed is not included in this response and is composed of all items from all feeds
|
||||
that belong to a given group. For the “Kindling” super group and all user created groups the items
|
||||
should be limited to feeds with an is_spark equal to 0.
|
||||
|
||||
For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
|
||||
*/
|
||||
func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
||||
|
||||
feeds, err := c.store.Feeds(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var result feedsResponse
|
||||
result.Feeds = make([]feed, 0)
|
||||
for _, f := range feeds {
|
||||
subscripion := feed{
|
||||
ID: f.ID,
|
||||
Title: f.Title,
|
||||
URL: f.FeedURL,
|
||||
SiteURL: f.SiteURL,
|
||||
IsSpark: 0,
|
||||
LastUpdated: f.CheckedAt.Unix(),
|
||||
}
|
||||
|
||||
if f.Icon != nil {
|
||||
subscripion.FaviconID = f.Icon.IconID
|
||||
}
|
||||
|
||||
result.Feeds = append(result.Feeds, subscripion)
|
||||
}
|
||||
|
||||
result.FeedsGroups = c.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the favicons argument will return one additional member:
|
||||
|
||||
favicons contains an array of favicon objects
|
||||
|
||||
A favicon object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
data (base64 encoded image data; prefixed by image type)
|
||||
|
||||
An example data value:
|
||||
|
||||
image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
|
||||
|
||||
The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML.
|
||||
A PHP/HTML example:
|
||||
|
||||
echo '<img src="data:'.$favicon['data'].'">';
|
||||
*/
|
||||
func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
|
||||
|
||||
icons, err := c.store.Icons(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var result faviconsResponse
|
||||
for _, i := range icons {
|
||||
result.Favicons = append(result.Favicons, favicon{
|
||||
ID: i.ID,
|
||||
Data: i.DataURL(),
|
||||
})
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the items argument will return two additional members:
|
||||
|
||||
items contains an array of item objects
|
||||
total_items contains the total number of items stored in the database (added in API version 2)
|
||||
|
||||
An item object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
feed_id (positive integer)
|
||||
title (utf-8 string)
|
||||
author (utf-8 string)
|
||||
html (utf-8 string)
|
||||
url (utf-8 string)
|
||||
is_saved (boolean integer)
|
||||
is_read (boolean integer)
|
||||
created_on_time (Unix timestamp/integer)
|
||||
|
||||
Most servers won’t have enough memory allocated to PHP to dump all items at once.
|
||||
Three optional arguments control determine the items included in the response.
|
||||
|
||||
Use the since_id argument with the highest id of locally cached items to request 50 additional items.
|
||||
Repeat until the items array in the response is empty.
|
||||
|
||||
Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items.
|
||||
Repeat until the items array in the response is empty. (added in API version 2)
|
||||
|
||||
Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items.
|
||||
(added in API version 2)
|
||||
|
||||
*/
|
||||
func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
var result itemsResponse
|
||||
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching items for userID=%d", userID)
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithLimit(50)
|
||||
builder.WithOrder("id")
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
|
||||
sinceID := request.QueryIntegerParam("since_id", 0)
|
||||
if sinceID > 0 {
|
||||
builder.WithGreaterThanEntryID(int64(sinceID))
|
||||
}
|
||||
|
||||
maxID := request.QueryIntegerParam("max_id", 0)
|
||||
if maxID > 0 {
|
||||
builder.WithOffset(maxID)
|
||||
}
|
||||
|
||||
csvItemIDs := request.QueryStringParam("with_ids", "")
|
||||
if csvItemIDs != "" {
|
||||
var itemIDs []int64
|
||||
|
||||
for _, strItemID := range strings.Split(csvItemIDs, ",") {
|
||||
strItemID = strings.TrimSpace(strItemID)
|
||||
itemID, _ := strconv.Atoi(strItemID)
|
||||
itemIDs = append(itemIDs, int64(itemID))
|
||||
}
|
||||
|
||||
builder.WithEntryIDs(itemIDs)
|
||||
}
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
result.Total, err = builder.CountEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
isRead := 0
|
||||
if entry.Status == model.EntryStatusRead {
|
||||
isRead = 1
|
||||
}
|
||||
|
||||
isSaved := 0
|
||||
if entry.Starred {
|
||||
isSaved = 1
|
||||
}
|
||||
|
||||
result.Items = append(result.Items, item{
|
||||
ID: entry.ID,
|
||||
FeedID: entry.FeedID,
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
HTML: entry.Content,
|
||||
URL: entry.URL,
|
||||
IsSaved: isSaved,
|
||||
IsRead: isRead,
|
||||
CreatedAt: entry.Date.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
|
||||
with the remote Fever installation.
|
||||
|
||||
A request with the unread_item_ids argument will return one additional member:
|
||||
unread_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var itemIDs []string
|
||||
for _, entry := range entries {
|
||||
itemIDs = append(itemIDs, strconv.FormatInt(entry.ID, 10))
|
||||
}
|
||||
|
||||
var result unreadResponse
|
||||
result.ItemIDs = strings.Join(itemIDs, ",")
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
|
||||
with the remote Fever installation.
|
||||
|
||||
A request with the saved_item_ids argument will return one additional member:
|
||||
|
||||
saved_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStarred()
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var itemsIDs []string
|
||||
for _, entryID := range entryIDs {
|
||||
itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
|
||||
}
|
||||
|
||||
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
||||
result.SetCommonValues()
|
||||
response.JSON().Standard(result)
|
||||
}
|
||||
|
||||
/*
|
||||
mark=item
|
||||
as=? where ? is replaced with read, saved or unsaved
|
||||
id=? where ? is replaced with the id of the item to modify
|
||||
*/
|
||||
func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
|
||||
|
||||
entryID := request.FormIntegerValue("id")
|
||||
if entryID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch request.FormValue("as") {
|
||||
case "read":
|
||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||
case "unread":
|
||||
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||
case "saved", "unsaved":
|
||||
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := c.store.Integration(userID)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
mark=? where ? is replaced with feed or group
|
||||
as=read
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
|
||||
|
||||
feedID := request.FormIntegerValue("id")
|
||||
if feedID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithFeedID(feedID)
|
||||
|
||||
before := request.FormIntegerValue("before")
|
||||
if before > 0 {
|
||||
t := time.Unix(before, 0)
|
||||
builder.Before(&t)
|
||||
}
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
mark=? where ? is replaced with feed or group
|
||||
as=read
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
|
||||
userID := ctx.UserID()
|
||||
logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
|
||||
|
||||
groupID := request.FormIntegerValue("id")
|
||||
if groupID < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder := c.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
builder.WithCategoryID(groupID)
|
||||
|
||||
before := request.FormIntegerValue("before")
|
||||
if before > 0 {
|
||||
t := time.Unix(before, 0)
|
||||
builder.Before(&t)
|
||||
}
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
response.JSON().ServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
response.JSON().Standard(newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
A feeds_group object has the following members:
|
||||
|
||||
group_id (positive integer)
|
||||
feed_ids (string/comma-separated list of positive integers)
|
||||
|
||||
*/
|
||||
func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
|
||||
feedsGroupedByCategory := make(map[int64][]string)
|
||||
for _, feed := range feeds {
|
||||
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
|
||||
}
|
||||
|
||||
result := make([]feedsGroups, 0)
|
||||
for categoryID, feedIDs := range feedsGroupedByCategory {
|
||||
result = append(result, feedsGroups{
|
||||
GroupID: categoryID,
|
||||
FeedIDs: strings.Join(feedIDs, ","),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NewController returns a new Fever API.
|
||||
func NewController(store *storage.Storage) *Controller {
|
||||
return &Controller{store: store}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fever // import "miniflux.app/fever"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/integration"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/storage"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Serve handles Fever API calls.
|
||||
func Serve(router *mux.Router, store *storage.Storage) {
|
||||
handler := &handler{store}
|
||||
|
||||
sr := router.PathPrefix("/fever").Subrouter()
|
||||
sr.Use(newMiddleware(store).serve)
|
||||
sr.HandleFunc("/", handler.serve).Name("feverEndpoint")
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case request.HasQueryParam(r, "groups"):
|
||||
h.handleGroups(w, r)
|
||||
case request.HasQueryParam(r, "feeds"):
|
||||
h.handleFeeds(w, r)
|
||||
case request.HasQueryParam(r, "favicons"):
|
||||
h.handleFavicons(w, r)
|
||||
case request.HasQueryParam(r, "unread_item_ids"):
|
||||
h.handleUnreadItems(w, r)
|
||||
case request.HasQueryParam(r, "saved_item_ids"):
|
||||
h.handleSavedItems(w, r)
|
||||
case request.HasQueryParam(r, "items"):
|
||||
h.handleItems(w, r)
|
||||
case r.FormValue("mark") == "item":
|
||||
h.handleWriteItems(w, r)
|
||||
case r.FormValue("mark") == "feed":
|
||||
h.handleWriteFeeds(w, r)
|
||||
case r.FormValue("mark") == "group":
|
||||
h.handleWriteGroups(w, r)
|
||||
default:
|
||||
json.OK(w, r, newBaseResponse())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the groups argument will return two additional members:
|
||||
|
||||
groups contains an array of group objects
|
||||
feeds_groups contains an array of feeds_group objects
|
||||
|
||||
A group object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
title (utf-8 string)
|
||||
|
||||
The feeds_group object is documented under “Feeds/Groups Relationships.”
|
||||
|
||||
The “Kindling” super group is not included in this response and is composed of all feeds with
|
||||
an is_spark equal to 0.
|
||||
|
||||
The “Sparks” super group is not included in this response and is composed of all feeds with an
|
||||
is_spark equal to 1.
|
||||
|
||||
*/
|
||||
func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching groups for user #%d", userID)
|
||||
|
||||
categories, err := h.store.Categories(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
feeds, err := h.store.Feeds(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var result groupsResponse
|
||||
for _, category := range categories {
|
||||
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
|
||||
}
|
||||
|
||||
result.FeedsGroups = h.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the feeds argument will return two additional members:
|
||||
|
||||
feeds contains an array of group objects
|
||||
feeds_groups contains an array of feeds_group objects
|
||||
|
||||
A feed object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
favicon_id (positive integer)
|
||||
title (utf-8 string)
|
||||
url (utf-8 string)
|
||||
site_url (utf-8 string)
|
||||
is_spark (boolean integer)
|
||||
last_updated_on_time (Unix timestamp/integer)
|
||||
|
||||
The feeds_group object is documented under “Feeds/Groups Relationships.”
|
||||
|
||||
The “All Items” super feed is not included in this response and is composed of all items from all feeds
|
||||
that belong to a given group. For the “Kindling” super group and all user created groups the items
|
||||
should be limited to feeds with an is_spark equal to 0.
|
||||
|
||||
For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
|
||||
*/
|
||||
func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
|
||||
|
||||
feeds, err := h.store.Feeds(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var result feedsResponse
|
||||
result.Feeds = make([]feed, 0)
|
||||
for _, f := range feeds {
|
||||
subscripion := feed{
|
||||
ID: f.ID,
|
||||
Title: f.Title,
|
||||
URL: f.FeedURL,
|
||||
SiteURL: f.SiteURL,
|
||||
IsSpark: 0,
|
||||
LastUpdated: f.CheckedAt.Unix(),
|
||||
}
|
||||
|
||||
if f.Icon != nil {
|
||||
subscripion.FaviconID = f.Icon.IconID
|
||||
}
|
||||
|
||||
result.Feeds = append(result.Feeds, subscripion)
|
||||
}
|
||||
|
||||
result.FeedsGroups = h.buildFeedGroups(feeds)
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the favicons argument will return one additional member:
|
||||
|
||||
favicons contains an array of favicon objects
|
||||
|
||||
A favicon object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
data (base64 encoded image data; prefixed by image type)
|
||||
|
||||
An example data value:
|
||||
|
||||
image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
|
||||
|
||||
The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML.
|
||||
A PHP/HTML example:
|
||||
|
||||
echo '<img src="data:'.$favicon['data'].'">';
|
||||
*/
|
||||
func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching favicons for user #%d", userID)
|
||||
|
||||
icons, err := h.store.Icons(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var result faviconsResponse
|
||||
for _, i := range icons {
|
||||
result.Favicons = append(result.Favicons, favicon{
|
||||
ID: i.ID,
|
||||
Data: i.DataURL(),
|
||||
})
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
A request with the items argument will return two additional members:
|
||||
|
||||
items contains an array of item objects
|
||||
total_items contains the total number of items stored in the database (added in API version 2)
|
||||
|
||||
An item object has the following members:
|
||||
|
||||
id (positive integer)
|
||||
feed_id (positive integer)
|
||||
title (utf-8 string)
|
||||
author (utf-8 string)
|
||||
html (utf-8 string)
|
||||
url (utf-8 string)
|
||||
is_saved (boolean integer)
|
||||
is_read (boolean integer)
|
||||
created_on_time (Unix timestamp/integer)
|
||||
|
||||
Most servers won’t have enough memory allocated to PHP to dump all items at once.
|
||||
Three optional arguments control determine the items included in the response.
|
||||
|
||||
Use the since_id argument with the highest id of locally cached items to request 50 additional items.
|
||||
Repeat until the items array in the response is empty.
|
||||
|
||||
Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items.
|
||||
Repeat until the items array in the response is empty. (added in API version 2)
|
||||
|
||||
Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items.
|
||||
(added in API version 2)
|
||||
|
||||
*/
|
||||
func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
var result itemsResponse
|
||||
|
||||
userID := request.UserID(r)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithLimit(50)
|
||||
builder.WithOrder("id")
|
||||
builder.WithDirection(model.DefaultSortingDirection)
|
||||
|
||||
switch {
|
||||
case request.HasQueryParam(r, "since_id"):
|
||||
sinceID := request.QueryInt64Param(r, "since_id", 0)
|
||||
if sinceID > 0 {
|
||||
logger.Debug("[Fever] Fetching items since #%d for user #%d", sinceID, userID)
|
||||
builder.AfterEntryID(sinceID)
|
||||
}
|
||||
case request.HasQueryParam(r, "max_id"):
|
||||
maxID := request.QueryInt64Param(r, "max_id", 0)
|
||||
if maxID == 0 {
|
||||
logger.Debug("[Fever] Fetching most recent items for user #%d", userID)
|
||||
builder.WithDirection("desc")
|
||||
} else if maxID > 0 {
|
||||
logger.Debug("[Fever] Fetching items before #%d for user #%d", maxID, userID)
|
||||
builder.BeforeEntryID(maxID)
|
||||
builder.WithDirection("desc")
|
||||
}
|
||||
case request.HasQueryParam(r, "with_ids"):
|
||||
csvItemIDs := request.QueryStringParam(r, "with_ids", "")
|
||||
if csvItemIDs != "" {
|
||||
var itemIDs []int64
|
||||
|
||||
for _, strItemID := range strings.Split(csvItemIDs, ",") {
|
||||
strItemID = strings.TrimSpace(strItemID)
|
||||
itemID, _ := strconv.ParseInt(strItemID, 10, 64)
|
||||
itemIDs = append(itemIDs, itemID)
|
||||
}
|
||||
|
||||
builder.WithEntryIDs(itemIDs)
|
||||
}
|
||||
default:
|
||||
logger.Debug("[Fever] Fetching oldest items for user #%d", userID)
|
||||
}
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
builder = h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
result.Total, err = builder.CountEntries()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
result.Items = make([]item, 0)
|
||||
for _, entry := range entries {
|
||||
isRead := 0
|
||||
if entry.Status == model.EntryStatusRead {
|
||||
isRead = 1
|
||||
}
|
||||
|
||||
isSaved := 0
|
||||
if entry.Starred {
|
||||
isSaved = 1
|
||||
}
|
||||
|
||||
result.Items = append(result.Items, item{
|
||||
ID: entry.ID,
|
||||
FeedID: entry.FeedID,
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
HTML: entry.Content,
|
||||
URL: entry.URL,
|
||||
IsSaved: isSaved,
|
||||
IsRead: isRead,
|
||||
CreatedAt: entry.Date.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
|
||||
with the remote Fever installation.
|
||||
|
||||
A request with the unread_item_ids argument will return one additional member:
|
||||
unread_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching unread items for user #%d", userID)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStatus(model.EntryStatusUnread)
|
||||
rawEntryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var itemIDs []string
|
||||
for _, entryID := range rawEntryIDs {
|
||||
itemIDs = append(itemIDs, strconv.FormatInt(entryID, 10))
|
||||
}
|
||||
|
||||
var result unreadResponse
|
||||
result.ItemIDs = strings.Join(itemIDs, ",")
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
|
||||
with the remote Fever installation.
|
||||
|
||||
A request with the saved_item_ids argument will return one additional member:
|
||||
|
||||
saved_item_ids (string/comma-separated list of positive integers)
|
||||
*/
|
||||
func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Fetching saved items for user #%d", userID)
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithStarred()
|
||||
|
||||
entryIDs, err := builder.GetEntryIDs()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var itemsIDs []string
|
||||
for _, entryID := range entryIDs {
|
||||
itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
|
||||
}
|
||||
|
||||
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
|
||||
result.SetCommonValues()
|
||||
json.OK(w, r, result)
|
||||
}
|
||||
|
||||
/*
|
||||
mark=item
|
||||
as=? where ? is replaced with read, saved or unsaved
|
||||
id=? where ? is replaced with the id of the item to modify
|
||||
*/
|
||||
func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
logger.Debug("[Fever] Receiving mark=item call for user #%d", userID)
|
||||
|
||||
entryID := request.FormInt64Value(r, "id")
|
||||
if entryID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
logger.Debug("[Fever] Marking entry #%d but not found, ignored", entryID)
|
||||
json.OK(w, r, newBaseResponse())
|
||||
return
|
||||
}
|
||||
|
||||
switch r.FormValue("as") {
|
||||
case "read":
|
||||
logger.Debug("[Fever] Mark entry #%d as read for user #%d", entryID, userID)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
|
||||
case "unread":
|
||||
logger.Debug("[Fever] Mark entry #%d as unread for user #%d", entryID, userID)
|
||||
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
|
||||
case "saved":
|
||||
logger.Debug("[Fever] Mark entry #%d as saved for user #%d", entryID, userID)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.store.Integration(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
integration.SendEntry(entry, settings)
|
||||
}()
|
||||
case "unsaved":
|
||||
logger.Debug("[Fever] Mark entry #%d as unsaved for user #%d", entryID, userID)
|
||||
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
json.OK(w, r, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
mark=feed
|
||||
as=read
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
feedID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] Mark feed #%d as read for user #%d before %v", feedID, userID, before)
|
||||
|
||||
if feedID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
|
||||
logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
json.OK(w, r, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
mark=group
|
||||
as=read
|
||||
id=? where ? is replaced with the id of the feed or group to modify
|
||||
before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
|
||||
*/
|
||||
func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
groupID := request.FormInt64Value(r, "id")
|
||||
before := time.Unix(request.FormInt64Value(r, "before"), 0)
|
||||
|
||||
logger.Debug("[Fever] Mark group #%d as read for user #%d before %v", groupID, userID, before)
|
||||
|
||||
if groupID < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
if groupID == 0 {
|
||||
err = h.store.MarkAllAsRead(userID)
|
||||
} else {
|
||||
err = h.store.MarkCategoryAsRead(userID, groupID, before)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("[Fever] MarkCategoryAsRead failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
json.OK(w, r, newBaseResponse())
|
||||
}
|
||||
|
||||
/*
|
||||
A feeds_group object has the following members:
|
||||
|
||||
group_id (positive integer)
|
||||
feed_ids (string/comma-separated list of positive integers)
|
||||
|
||||
*/
|
||||
func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups {
|
||||
feedsGroupedByCategory := make(map[int64][]string)
|
||||
for _, feed := range feeds {
|
||||
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
|
||||
}
|
||||
|
||||
result := make([]feedsGroups, 0)
|
||||
for categoryID, feedIDs := range feedsGroupedByCategory {
|
||||
result = append(result, feedsGroups{
|
||||
GroupID: categoryID,
|
||||
FeedIDs: strings.Join(feedIDs, ","),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fever // import "miniflux.app/fever"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/json"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/storage"
|
||||
)
|
||||
|
||||
type middleware struct {
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func newMiddleware(s *storage.Storage) *middleware {
|
||||
return &middleware{s}
|
||||
}
|
||||
|
||||
func (m *middleware) serve(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.ClientIP(r)
|
||||
apiKey := r.FormValue("api_key")
|
||||
if apiKey == "" {
|
||||
logger.Info("[Fever] [ClientIP=%s] No API key provided", clientIP)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.store.UserByFeverToken(apiKey)
|
||||
if err != nil {
|
||||
logger.Error("[Fever] %v", err)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
logger.Info("[Fever] [ClientIP=%s] No user found with this API key", clientIP)
|
||||
json.OK(w, r, newAuthFailureResponse())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("[Fever] [ClientIP=%s] User #%d is authenticated with user agent %q", clientIP, user.ID, r.UserAgent())
|
||||
m.store.SetLastLogin(user.ID)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
|
||||
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
|
||||
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
|
||||
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fever // import "miniflux.app/fever"
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type baseResponse struct {
|
||||
Version int `json:"api_version"`
|
||||
Authenticated int `json:"auth"`
|
||||
LastRefresh int64 `json:"last_refreshed_on_time"`
|
||||
}
|
||||
|
||||
func (b *baseResponse) SetCommonValues() {
|
||||
b.Version = 3
|
||||
b.Authenticated = 1
|
||||
b.LastRefresh = time.Now().Unix()
|
||||
}
|
||||
|
||||
/*
|
||||
The default response is a JSON object containing two members:
|
||||
|
||||
api_version contains the version of the API responding (positive integer)
|
||||
auth whether the request was successfully authenticated (boolean integer)
|
||||
|
||||
The API can also return XML by passing xml as the optional value of the api argument like so:
|
||||
|
||||
http://yourdomain.com/fever/?api=xml
|
||||
|
||||
The top level XML element is named response.
|
||||
|
||||
The response to each successfully authenticated request will have auth set to 1 and include
|
||||
at least one additional member:
|
||||
|
||||
last_refreshed_on_time contains the time of the most recently refreshed (not updated)
|
||||
feed (Unix timestamp/integer)
|
||||
|
||||
*/
|
||||
func newBaseResponse() baseResponse {
|
||||
r := baseResponse{}
|
||||
r.SetCommonValues()
|
||||
return r
|
||||
}
|
||||
|
||||
func newAuthFailureResponse() baseResponse {
|
||||
return baseResponse{Version: 3, Authenticated: 0}
|
||||
}
|
||||
|
||||
type groupsResponse struct {
|
||||
baseResponse
|
||||
Groups []group `json:"groups"`
|
||||
FeedsGroups []feedsGroups `json:"feeds_groups"`
|
||||
}
|
||||
|
||||
type feedsResponse struct {
|
||||
baseResponse
|
||||
Feeds []feed `json:"feeds"`
|
||||
FeedsGroups []feedsGroups `json:"feeds_groups"`
|
||||
}
|
||||
|
||||
type faviconsResponse struct {
|
||||
baseResponse
|
||||
Favicons []favicon `json:"favicons"`
|
||||
}
|
||||
|
||||
type itemsResponse struct {
|
||||
baseResponse
|
||||
Items []item `json:"items"`
|
||||
Total int `json:"total_items"`
|
||||
}
|
||||
|
||||
type unreadResponse struct {
|
||||
baseResponse
|
||||
ItemIDs string `json:"unread_item_ids"`
|
||||
}
|
||||
|
||||
type savedResponse struct {
|
||||
baseResponse
|
||||
ItemIDs string `json:"saved_item_ids"`
|
||||
}
|
||||
|
||||
type group struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type feedsGroups struct {
|
||||
GroupID int64 `json:"group_id"`
|
||||
FeedIDs string `json:"feed_ids"`
|
||||
}
|
||||
|
||||
type feed struct {
|
||||
ID int64 `json:"id"`
|
||||
FaviconID int64 `json:"favicon_id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
IsSpark int `json:"is_spark"`
|
||||
LastUpdated int64 `json:"last_updated_on_time"`
|
||||
}
|
||||
|
||||
type item struct {
|
||||
ID int64 `json:"id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
HTML string `json:"html"`
|
||||
URL string `json:"url"`
|
||||
IsSaved int `json:"is_saved"`
|
||||
IsRead int `json:"is_read"`
|
||||
CreatedAt int64 `json:"created_on_time"`
|
||||
}
|
||||
|
||||
type favicon struct {
|
||||
ID int64 `json:"id"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package filter implements a content filter to rewrite image links.
|
||||
|
||||
*/
|
||||
package filter
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/miniflux/miniflux/http/route"
|
||||
"github.com/miniflux/miniflux/url"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ImageProxyFilter rewrites image tag URLs without HTTPS to local proxy URL
|
||||
func ImageProxyFilter(router *mux.Router, data string) string {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
doc.Find("img").Each(func(i int, img *goquery.Selection) {
|
||||
if srcAttr, ok := img.Attr("src"); ok {
|
||||
if !url.IsHTTPS(srcAttr) {
|
||||
img.SetAttr("src", Proxify(router, srcAttr))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
output, _ := doc.Find("body").First().Html()
|
||||
return output
|
||||
}
|
||||
|
||||
// Proxify returns a proxified link.
|
||||
func Proxify(router *mux.Router, link string) string {
|
||||
// We use base64 url encoding to avoid slash in the URL.
|
||||
return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func TestProxyFilterWithHttp(t *testing.T) {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyFilter(r, input)
|
||||
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttps(t *testing.T) {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ImageProxyFilter(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
}
|
||||
}
|
||||
+176
-75
@@ -16,17 +16,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/tdewolff/minify"
|
||||
"github.com/tdewolff/minify/css"
|
||||
"github.com/tdewolff/minify/js"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
"github.com/tdewolff/minify/v2/css"
|
||||
"github.com/tdewolff/minify/v2/js"
|
||||
)
|
||||
|
||||
const tpl = `// Code generated by go generate; DO NOT EDIT.
|
||||
// {{ .Timestamp }}
|
||||
|
||||
package {{ .Package }}
|
||||
package {{ .Package }} // import "miniflux.app/{{ .ImportPath }}"
|
||||
|
||||
var {{ .Map }} = map[string]string{
|
||||
{{ range $constant, $content := .Files }}` + "\t" + `"{{ $constant }}": ` + "`{{ $content }}`" + `,
|
||||
@@ -37,84 +35,187 @@ var {{ .Map }}Checksums = map[string]string{
|
||||
{{ end }}}
|
||||
`
|
||||
|
||||
var generatedTpl = template.Must(template.New("").Parse(tpl))
|
||||
var bundleTpl = template.Must(template.New("").Parse(tpl))
|
||||
|
||||
type GeneratedFile struct {
|
||||
Package, Map string
|
||||
Timestamp time.Time
|
||||
Files map[string]string
|
||||
Checksums map[string]string
|
||||
type Bundle struct {
|
||||
Package string
|
||||
Map string
|
||||
ImportPath string
|
||||
Files map[string]string
|
||||
Checksums map[string]string
|
||||
}
|
||||
|
||||
func normalizeBasename(filename string) string {
|
||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
return strings.Replace(filename, " ", "_", -1)
|
||||
}
|
||||
|
||||
func generateFile(serializer, pkg, mapName, pattern, output string) {
|
||||
generatedFile := &GeneratedFile{
|
||||
Package: pkg,
|
||||
Map: mapName,
|
||||
Timestamp: time.Now(),
|
||||
Files: make(map[string]string),
|
||||
Checksums: make(map[string]string),
|
||||
}
|
||||
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for _, file := range files {
|
||||
basename := path.Base(file)
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch serializer {
|
||||
case "css":
|
||||
m := minify.New()
|
||||
m.AddFunc("text/css", css.Minify)
|
||||
content, err = m.Bytes("text/css", content)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
basename = normalizeBasename(basename)
|
||||
generatedFile.Files[basename] = string(content)
|
||||
case "js":
|
||||
m := minify.New()
|
||||
m.AddFunc("text/javascript", js.Minify)
|
||||
content, err = m.Bytes("text/javascript", content)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
basename = normalizeBasename(basename)
|
||||
generatedFile.Files[basename] = string(content)
|
||||
case "base64":
|
||||
encodedContent := base64.StdEncoding.EncodeToString(content)
|
||||
generatedFile.Files[basename] = encodedContent
|
||||
default:
|
||||
basename = normalizeBasename(basename)
|
||||
generatedFile.Files[basename] = string(content)
|
||||
}
|
||||
|
||||
generatedFile.Checksums[basename] = fmt.Sprintf("%x", sha256.Sum256(content))
|
||||
}
|
||||
|
||||
f, err := os.Create(output)
|
||||
func (b *Bundle) Write(filename string) {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
generatedTpl.Execute(f, generatedFile)
|
||||
bundleTpl.Execute(f, b)
|
||||
}
|
||||
|
||||
func NewBundle(pkg, mapName, importPath string) *Bundle {
|
||||
return &Bundle{
|
||||
Package: pkg,
|
||||
Map: mapName,
|
||||
ImportPath: importPath,
|
||||
Files: make(map[string]string),
|
||||
Checksums: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(filename string) []byte {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func checksum(data []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(data))
|
||||
}
|
||||
|
||||
func basename(filename string) string {
|
||||
return path.Base(filename)
|
||||
}
|
||||
|
||||
func stripExtension(filename string) string {
|
||||
filename = strings.TrimSuffix(filename, path.Ext(filename))
|
||||
return strings.Replace(filename, " ", "_", -1)
|
||||
}
|
||||
|
||||
func glob(pattern string) []string {
|
||||
// There is no Glob function in path package, so we have to use filepath and replace in case of Windows
|
||||
files, _ := filepath.Glob(pattern)
|
||||
for i := range files {
|
||||
if strings.Contains(files[i], "\\") {
|
||||
files[i] = strings.Replace(files[i], "\\", "/", -1)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func concat(files []string) string {
|
||||
var b strings.Builder
|
||||
for _, file := range files {
|
||||
b.Write(readFile(file))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func generateJSBundle(bundleFile string, bundleFiles map[string][]string, prefixes, suffixes map[string]string) {
|
||||
bundle := NewBundle("static", "Javascripts", "ui/static")
|
||||
m := minify.New()
|
||||
m.AddFunc("text/javascript", js.Minify)
|
||||
|
||||
for name, srcFiles := range bundleFiles {
|
||||
var b strings.Builder
|
||||
|
||||
if prefix, found := prefixes[name]; found {
|
||||
b.WriteString(prefix)
|
||||
}
|
||||
|
||||
b.WriteString(concat(srcFiles))
|
||||
|
||||
if suffix, found := suffixes[name]; found {
|
||||
b.WriteString(suffix)
|
||||
}
|
||||
|
||||
minifiedData, err := m.String("text/javascript", b.String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bundle.Files[name] = minifiedData
|
||||
bundle.Checksums[name] = checksum([]byte(minifiedData))
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateCSSBundle(bundleFile string, themes map[string][]string) {
|
||||
bundle := NewBundle("static", "Stylesheets", "ui/static")
|
||||
m := minify.New()
|
||||
m.AddFunc("text/css", css.Minify)
|
||||
|
||||
for theme, srcFiles := range themes {
|
||||
data := concat(srcFiles)
|
||||
minifiedData, err := m.String("text/css", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bundle.Files[theme] = minifiedData
|
||||
bundle.Checksums[theme] = checksum([]byte(minifiedData))
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateBinaryBundle(bundleFile string, srcFiles []string) {
|
||||
bundle := NewBundle("static", "Binaries", "ui/static")
|
||||
|
||||
for _, srcFile := range srcFiles {
|
||||
data := readFile(srcFile)
|
||||
filename := basename(srcFile)
|
||||
encodedData := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
bundle.Files[filename] = string(encodedData)
|
||||
bundle.Checksums[filename] = checksum(data)
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func generateBundle(bundleFile, pkg, mapName string, srcFiles []string) {
|
||||
bundle := NewBundle(pkg, mapName, pkg)
|
||||
|
||||
for _, srcFile := range srcFiles {
|
||||
data := readFile(srcFile)
|
||||
filename := stripExtension(basename(srcFile))
|
||||
|
||||
bundle.Files[filename] = string(data)
|
||||
bundle.Checksums[filename] = checksum(data)
|
||||
}
|
||||
|
||||
bundle.Write(bundleFile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
generateFile("none", "sql", "SqlMap", "sql/*.sql", "sql/sql.go")
|
||||
generateFile("base64", "static", "Binaries", "ui/static/bin/*", "ui/static/bin.go")
|
||||
generateFile("css", "static", "Stylesheets", "ui/static/css/*.css", "ui/static/css.go")
|
||||
generateFile("js", "static", "Javascript", "ui/static/js/*.js", "ui/static/js.go")
|
||||
generateFile("none", "template", "templateViewsMap", "template/html/*.html", "template/views.go")
|
||||
generateFile("none", "template", "templateCommonMap", "template/html/common/*.html", "template/common.go")
|
||||
generateFile("none", "locale", "translations", "locale/translations/*.json", "locale/translations.go")
|
||||
generateJSBundle("ui/static/js.go", map[string][]string{
|
||||
"app": []string{
|
||||
"ui/static/js/dom_helper.js",
|
||||
"ui/static/js/touch_handler.js",
|
||||
"ui/static/js/keyboard_handler.js",
|
||||
"ui/static/js/request_builder.js",
|
||||
"ui/static/js/modal_handler.js",
|
||||
"ui/static/js/app.js",
|
||||
"ui/static/js/bootstrap.js",
|
||||
},
|
||||
"service-worker": []string{
|
||||
"ui/static/js/service_worker.js",
|
||||
},
|
||||
}, map[string]string{
|
||||
"app": "(function(){'use strict';",
|
||||
"sw": "'use strict';",
|
||||
}, map[string]string{
|
||||
"app": "})();",
|
||||
})
|
||||
|
||||
generateCSSBundle("ui/static/css.go", map[string][]string{
|
||||
"light_serif": []string{"ui/static/css/light.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
|
||||
"light_sans_serif": []string{"ui/static/css/light.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
|
||||
"dark_serif": []string{"ui/static/css/dark.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
|
||||
"dark_sans_serif": []string{"ui/static/css/dark.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
|
||||
"system_serif": []string{"ui/static/css/system.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
|
||||
"system_sans_serif": []string{"ui/static/css/system.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
|
||||
})
|
||||
|
||||
generateBinaryBundle("ui/static/bin.go", glob("ui/static/bin/*"))
|
||||
|
||||
generateBundle("template/views.go", "template", "templateViewsMap", glob("template/html/*.html"))
|
||||
generateBundle("template/common.go", "template", "templateCommonMap", glob("template/html/common/*.html"))
|
||||
generateBundle("locale/translations.go", "locale", "translations", glob("locale/translations/*.json"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
module miniflux.app
|
||||
|
||||
// +heroku goVersion go1.15
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.6.1
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/lib/pq v1.9.0
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.9.11 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
|
||||
mvdan.cc/xurls/v2 v2.2.0
|
||||
)
|
||||
|
||||
go 1.15
|
||||
@@ -0,0 +1,584 @@
|
||||
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk=
|
||||
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/felixge/httpsnoop v1.0.0 h1:gh8fMGz0rlOv/1WmRZm7OgncIOTsAj21iNJot48omJQ=
|
||||
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 h1:yrv1uUvgXH/tEat+wdvJMRJ4g51GlIydtDpU9pFjaaI=
|
||||
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 h1:5vu7C+63KTbsSNnLhrgB98Sqy8MNVSW8FdhkcWA/3Rk=
|
||||
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||
github.com/google/go-dap v0.3.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2 h1:9kYRAm87/M5VL+HWegDGIorBWDiErrZrksLKTJBF2IQ=
|
||||
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2/go.mod h1:8mKJVJkyMI20ifXXO0zlV3sh3FrtjvltAeBqz38clBE=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
||||
github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
|
||||
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
|
||||
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d h1:4Jn2kzSKcpxxwpJdyWxfNWKCYe4Uki2VibkExYmBLiA=
|
||||
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw=
|
||||
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
|
||||
github.com/tdewolff/minify/v2 v2.9.11 h1:8o6hclGwxm6MNwTPHabvdND5SghhHs0bn+3/+uAf0yQ=
|
||||
github.com/tdewolff/minify/v2 v2.9.11/go.mod h1:YZk0lGOc6CvQrqvm5f7V3ihaq3QUd9acS4HESdVDOaM=
|
||||
github.com/tdewolff/parse/v2 v2.5.8 h1:vutkOO9Xi3DehIzCLHqvMM2hFXo54S0iDvIG/hYznnE=
|
||||
github.com/tdewolff/parse/v2 v2.5.8/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
|
||||
go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
|
||||
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
|
||||
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
|
||||
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
"github.com/miniflux/miniflux/version"
|
||||
)
|
||||
|
||||
const requestTimeout = 300
|
||||
const maxBodySize = 1024 * 1024 * 15
|
||||
|
||||
// Client is a HTTP Client :)
|
||||
type Client struct {
|
||||
url string
|
||||
etagHeader string
|
||||
lastModifiedHeader string
|
||||
authorizationHeader string
|
||||
username string
|
||||
password string
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// Get execute a GET HTTP request.
|
||||
func (c *Client) Get() (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodGet, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostForm execute a POST HTTP request with form values.
|
||||
func (c *Client) PostForm(values url.Values) (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostJSON execute a POST HTTP request with JSON payload.
|
||||
func (c *Client) PostJSON(data interface{}) (*Response, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] url=%s", c.url))
|
||||
|
||||
client := c.buildClient()
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.ContentLength > maxBodySize {
|
||||
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
|
||||
}
|
||||
|
||||
response := &Response{
|
||||
Body: resp.Body,
|
||||
StatusCode: resp.StatusCode,
|
||||
EffectiveURL: resp.Request.URL.String(),
|
||||
LastModified: resp.Header.Get("Last-Modified"),
|
||||
ETag: resp.Header.Get("ETag"),
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ContentLength: resp.ContentLength,
|
||||
}
|
||||
|
||||
logger.Debug("[HttpClient:%s] OriginalURL=%s, StatusCode=%d, ContentLength=%d, ContentType=%s, ETag=%s, LastModified=%s, EffectiveURL=%s",
|
||||
request.Method,
|
||||
c.url,
|
||||
response.StatusCode,
|
||||
resp.ContentLength,
|
||||
response.ContentType,
|
||||
response.ETag,
|
||||
response.LastModified,
|
||||
response.EffectiveURL,
|
||||
)
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
|
||||
request, err := http.NewRequest(method, c.url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.username != "" && c.password != "" {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
|
||||
request.Header = c.buildHeaders()
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildClient() http.Client {
|
||||
client := http.Client{Timeout: time.Duration(requestTimeout * time.Second)}
|
||||
if c.Insecure {
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) buildHeaders() http.Header {
|
||||
headers := make(http.Header)
|
||||
headers.Add("User-Agent", "Mozilla/5.0 (compatible; Miniflux/"+version.Version+"; +https://miniflux.net)")
|
||||
headers.Add("Accept", "*/*")
|
||||
|
||||
if c.etagHeader != "" {
|
||||
headers.Add("If-None-Match", c.etagHeader)
|
||||
}
|
||||
|
||||
if c.lastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.lastModifiedHeader)
|
||||
}
|
||||
|
||||
if c.authorizationHeader != "" {
|
||||
headers.Add("Authorization", c.authorizationHeader)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// NewClient returns a new HTTP client.
|
||||
func NewClient(url string) *Client {
|
||||
return &Client{url: url, Insecure: false}
|
||||
}
|
||||
|
||||
// NewClientWithCredentials returns a new HTTP client that requires authentication.
|
||||
func NewClientWithCredentials(url, username, password string) *Client {
|
||||
return &Client{url: url, Insecure: false, username: username, password: password}
|
||||
}
|
||||
|
||||
// NewClientWithAuthorization returns a new client with a custom authorization header.
|
||||
func NewClientWithAuthorization(url, authorization string) *Client {
|
||||
return &Client{url: url, Insecure: false, authorizationHeader: authorization}
|
||||
}
|
||||
|
||||
// NewClientWithCacheHeaders returns a new HTTP client that send cache headers.
|
||||
func NewClientWithCacheHeaders(url, etagHeader, lastModifiedHeader string) *Client {
|
||||
return &Client{url: url, etagHeader: etagHeader, lastModifiedHeader: lastModifiedHeader, Insecure: false}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/config"
|
||||
"miniflux.app/errors"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/timer"
|
||||
url_helper "miniflux.app/url"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCertificate = "Invalid SSL certificate (original error: %q)"
|
||||
errTemporaryNetworkOperation = "This website is temporarily unreachable (original error: %q)"
|
||||
errPermanentNetworkOperation = "This website is permanently unreachable (original error: %q)"
|
||||
errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
|
||||
)
|
||||
|
||||
// Client builds and executes HTTP requests.
|
||||
type Client struct {
|
||||
inputURL string
|
||||
|
||||
requestURL string
|
||||
requestEtagHeader string
|
||||
requestLastModifiedHeader string
|
||||
requestAuthorizationHeader string
|
||||
requestUsername string
|
||||
requestPassword string
|
||||
requestUserAgent string
|
||||
|
||||
useProxy bool
|
||||
doNotFollowRedirects bool
|
||||
|
||||
ClientTimeout int
|
||||
ClientMaxBodySize int64
|
||||
ClientProxyURL string
|
||||
}
|
||||
|
||||
// New initializes a new HTTP client.
|
||||
func New(url string) *Client {
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
ClientTimeout: defaultHTTPClientTimeout,
|
||||
ClientMaxBodySize: defaultHTTPClientMaxBodySize,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithConfig initializes a new HTTP client with application config options.
|
||||
func NewClientWithConfig(url string, opts *config.Options) *Client {
|
||||
return &Client{
|
||||
inputURL: url,
|
||||
requestUserAgent: opts.HTTPClientUserAgent(),
|
||||
ClientTimeout: opts.HTTPClientTimeout(),
|
||||
ClientMaxBodySize: opts.HTTPClientMaxBodySize(),
|
||||
ClientProxyURL: opts.HTTPClientProxy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
etagHeader := c.requestEtagHeader
|
||||
if c.requestEtagHeader == "" {
|
||||
etagHeader = "None"
|
||||
}
|
||||
|
||||
lastModifiedHeader := c.requestLastModifiedHeader
|
||||
if c.requestLastModifiedHeader == "" {
|
||||
lastModifiedHeader = "None"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`InputURL=%q RequestURL=%q ETag=%s LastModified=%s Auth=%v UserAgent=%q`,
|
||||
c.inputURL,
|
||||
c.requestURL,
|
||||
etagHeader,
|
||||
lastModifiedHeader,
|
||||
c.requestAuthorizationHeader != "" || (c.requestUsername != "" && c.requestPassword != ""),
|
||||
c.requestUserAgent,
|
||||
)
|
||||
}
|
||||
|
||||
// WithCredentials defines the username/password for HTTP Basic authentication.
|
||||
func (c *Client) WithCredentials(username, password string) *Client {
|
||||
if username != "" && password != "" {
|
||||
c.requestUsername = username
|
||||
c.requestPassword = password
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithAuthorization defines the authorization HTTP header value.
|
||||
func (c *Client) WithAuthorization(authorization string) *Client {
|
||||
c.requestAuthorizationHeader = authorization
|
||||
return c
|
||||
}
|
||||
|
||||
// WithCacheHeaders defines caching headers.
|
||||
func (c *Client) WithCacheHeaders(etagHeader, lastModifiedHeader string) *Client {
|
||||
c.requestEtagHeader = etagHeader
|
||||
c.requestLastModifiedHeader = lastModifiedHeader
|
||||
return c
|
||||
}
|
||||
|
||||
// WithProxy enables proxy for the current HTTP request.
|
||||
func (c *Client) WithProxy() *Client {
|
||||
c.useProxy = true
|
||||
return c
|
||||
}
|
||||
|
||||
// WithoutRedirects disables HTTP redirects.
|
||||
func (c *Client) WithoutRedirects() *Client {
|
||||
c.doNotFollowRedirects = true
|
||||
return c
|
||||
}
|
||||
|
||||
// WithUserAgent defines the User-Agent header to use for HTTP requests.
|
||||
func (c *Client) WithUserAgent(userAgent string) *Client {
|
||||
if userAgent != "" {
|
||||
c.requestUserAgent = userAgent
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Get performs a GET HTTP request.
|
||||
func (c *Client) Get() (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodGet, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostForm performs a POST HTTP request with form encoded values.
|
||||
func (c *Client) PostForm(values url.Values) (*Response, error) {
|
||||
request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
// PostJSON performs a POST HTTP request with a JSON payload.
|
||||
func (c *Client) PostJSON(data interface{}) (*Response, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
return c.executeRequest(request)
|
||||
}
|
||||
|
||||
func (c *Client) executeRequest(request *http.Request) (*Response, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] inputURL=%s", c.inputURL))
|
||||
|
||||
logger.Debug("[HttpClient:Before] Method=%s %s",
|
||||
request.Method,
|
||||
c.String(),
|
||||
)
|
||||
|
||||
client := c.buildClient()
|
||||
resp, err := client.Do(request)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
switch uerr.Err.(type) {
|
||||
case x509.CertificateInvalidError, x509.HostnameError:
|
||||
err = errors.NewLocalizedError(errInvalidCertificate, uerr.Err)
|
||||
case *net.OpError:
|
||||
if uerr.Err.(*net.OpError).Temporary() {
|
||||
err = errors.NewLocalizedError(errTemporaryNetworkOperation, uerr.Err)
|
||||
} else {
|
||||
err = errors.NewLocalizedError(errPermanentNetworkOperation, uerr.Err)
|
||||
}
|
||||
case net.Error:
|
||||
nerr := uerr.Err.(net.Error)
|
||||
if nerr.Timeout() {
|
||||
err = errors.NewLocalizedError(errRequestTimeout, c.ClientTimeout)
|
||||
} else if nerr.Temporary() {
|
||||
err = errors.NewLocalizedError(errTemporaryNetworkOperation, nerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.ContentLength > c.ClientMaxBodySize {
|
||||
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client: error while reading body %v", err)
|
||||
}
|
||||
|
||||
response := &Response{
|
||||
Body: bytes.NewReader(buf),
|
||||
StatusCode: resp.StatusCode,
|
||||
EffectiveURL: resp.Request.URL.String(),
|
||||
LastModified: resp.Header.Get("Last-Modified"),
|
||||
ETag: resp.Header.Get("ETag"),
|
||||
Expires: resp.Header.Get("Expires"),
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ContentLength: resp.ContentLength,
|
||||
}
|
||||
|
||||
logger.Debug("[HttpClient:After] Method=%s %s; Response => %s",
|
||||
request.Method,
|
||||
c.String(),
|
||||
response,
|
||||
)
|
||||
|
||||
// Ignore caching headers for feeds that do not want any cache.
|
||||
if resp.Header.Get("Expires") == "0" {
|
||||
logger.Debug("[HttpClient] Ignore caching headers for %q", response.EffectiveURL)
|
||||
response.ETag = ""
|
||||
response.LastModified = ""
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
|
||||
c.requestURL = url_helper.RequestURI(c.inputURL)
|
||||
request, err := http.NewRequest(method, c.requestURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header = c.buildHeaders()
|
||||
|
||||
if c.requestUsername != "" && c.requestPassword != "" {
|
||||
request.SetBasicAuth(c.requestUsername, c.requestPassword)
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildClient() http.Client {
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(c.ClientTimeout) * time.Second,
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
// Default is 30s.
|
||||
Timeout: 10 * time.Second,
|
||||
|
||||
// Default is 30s.
|
||||
KeepAlive: 15 * time.Second,
|
||||
}).DialContext,
|
||||
|
||||
// Default is 100.
|
||||
MaxIdleConns: 50,
|
||||
|
||||
// Default is 90s.
|
||||
IdleConnTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
if c.doNotFollowRedirects {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
|
||||
if c.useProxy && c.ClientProxyURL != "" {
|
||||
proxyURL, err := url.Parse(c.ClientProxyURL)
|
||||
if err != nil {
|
||||
logger.Error("[HttpClient] Proxy URL error: %v", err)
|
||||
} else {
|
||||
logger.Debug("[HttpClient] Use proxy: %s", proxyURL)
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
client.Transport = transport
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) buildHeaders() http.Header {
|
||||
headers := make(http.Header)
|
||||
headers.Add("Accept", "*/*")
|
||||
|
||||
if c.requestUserAgent != "" {
|
||||
headers.Add("User-Agent", c.requestUserAgent)
|
||||
}
|
||||
|
||||
if c.requestEtagHeader != "" {
|
||||
headers.Add("If-None-Match", c.requestEtagHeader)
|
||||
}
|
||||
|
||||
if c.requestLastModifiedHeader != "" {
|
||||
headers.Add("If-Modified-Since", c.requestLastModifiedHeader)
|
||||
}
|
||||
|
||||
if c.requestAuthorizationHeader != "" {
|
||||
headers.Add("Authorization", c.requestAuthorizationHeader)
|
||||
}
|
||||
|
||||
headers.Add("Connection", "close")
|
||||
return headers
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2020 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientWithDelay(t *testing.T) {
|
||||
clt := New("http://httpbin.org/delay/5")
|
||||
clt.ClientTimeout = 1
|
||||
_, err := clt.Get()
|
||||
if err == nil {
|
||||
t.Fatal(`The client should stops after 1 second`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithError(t *testing.T) {
|
||||
clt := New("http://httpbin.org/status/502")
|
||||
clt.ClientTimeout = 1
|
||||
response, err := clt.Get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if response.StatusCode != 502 {
|
||||
t.Fatalf(`Unexpected response status code: %d`, response.StatusCode)
|
||||
}
|
||||
|
||||
if !response.HasServerFailure() {
|
||||
t.Fatal(`A 500 error is considered as server failure`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithResponseTooLarge(t *testing.T) {
|
||||
clt := New("http://httpbin.org/bytes/100")
|
||||
clt.ClientMaxBodySize = 10
|
||||
_, err := clt.Get()
|
||||
if err == nil {
|
||||
t.Fatal(`The client should fails when reading a response too large`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWithBasicAuth(t *testing.T) {
|
||||
clt := New("http://httpbin.org/basic-auth/testuser/testpassword")
|
||||
clt.WithCredentials("testuser", "testpassword")
|
||||
_, err := clt.Get()
|
||||
if err != nil {
|
||||
t.Fatalf(`The client should be authenticated successfully: %v`, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2018 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
|
||||
Package client provides an HTTP client builder.
|
||||
|
||||
*/
|
||||
package client // import "miniflux.app/http/client"
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
)
|
||||
|
||||
var xmlEncodingRegex = regexp.MustCompile(`<\?xml(.*)encoding=["'](.+)["'](.*)\?>`)
|
||||
|
||||
// Response wraps a server response.
|
||||
type Response struct {
|
||||
Body io.Reader
|
||||
StatusCode int
|
||||
EffectiveURL string
|
||||
LastModified string
|
||||
ETag string
|
||||
Expires string
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
}
|
||||
|
||||
func (r *Response) String() string {
|
||||
return fmt.Sprintf(
|
||||
`StatusCode=%d EffectiveURL=%q LastModified=%q ETag=%s Expires=%s ContentType=%q ContentLength=%d`,
|
||||
r.StatusCode,
|
||||
r.EffectiveURL,
|
||||
r.LastModified,
|
||||
r.ETag,
|
||||
r.Expires,
|
||||
r.ContentType,
|
||||
r.ContentLength,
|
||||
)
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the resource doesn't exists anymore.
|
||||
func (r *Response) IsNotFound() bool {
|
||||
return r.StatusCode == 404 || r.StatusCode == 410
|
||||
}
|
||||
|
||||
// IsNotAuthorized returns true if the resource require authentication.
|
||||
func (r *Response) IsNotAuthorized() bool {
|
||||
return r.StatusCode == 401
|
||||
}
|
||||
|
||||
// HasServerFailure returns true if the status code represents a failure.
|
||||
func (r *Response) HasServerFailure() bool {
|
||||
return r.StatusCode >= 400
|
||||
}
|
||||
|
||||
// IsModified returns true if the resource has been modified.
|
||||
func (r *Response) IsModified(etag, lastModified string) bool {
|
||||
if r.StatusCode == 304 {
|
||||
return false
|
||||
}
|
||||
|
||||
if r.ETag != "" && r.ETag == etag {
|
||||
return false
|
||||
}
|
||||
|
||||
if r.LastModified != "" && r.LastModified == lastModified {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// EnsureUnicodeBody makes sure the body is encoded in UTF-8.
|
||||
//
|
||||
// If a charset other than UTF-8 is detected, we convert the document to UTF-8.
|
||||
// This is used by the scraper and feed readers.
|
||||
//
|
||||
// Do not forget edge cases:
|
||||
//
|
||||
// - Feeds with encoding specified only in Content-Type header and not in XML document
|
||||
// - Feeds with encoding specified in both places
|
||||
// - Feeds with encoding specified only in XML document and not in HTTP header
|
||||
// - Feeds with wrong encoding defined and already in UTF-8
|
||||
func (r *Response) EnsureUnicodeBody() (err error) {
|
||||
buffer, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Body = bytes.NewReader(buffer)
|
||||
if utf8.Valid(buffer) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(r.ContentType, "xml") {
|
||||
// We ignore documents with encoding specified in XML prolog.
|
||||
// This is going to be handled by the XML parser.
|
||||
length := 1024
|
||||
if len(buffer) < 1024 {
|
||||
length = len(buffer)
|
||||
}
|
||||
|
||||
if xmlEncodingRegex.Match(buffer[0:length]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
r.Body, err = charset.NewReader(r.Body, r.ContentType)
|
||||
return err
|
||||
}
|
||||
|
||||
// BodyAsString returns the response body as string.
|
||||
func (r *Response) BodyAsString() string {
|
||||
bytes, _ := ioutil.ReadAll(r.Body)
|
||||
return string(bytes)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
||||
// Use of this source code is governed by the Apache 2.0
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package client // import "miniflux.app/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestIsNotFound(t *testing.T) {
|
||||
scenarios := map[int]bool{
|
||||
200: false,
|
||||
404: true,
|
||||
410: true,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
r := &Response{StatusCode: input}
|
||||
actual := r.IsNotFound()
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotAuthorized(t *testing.T) {
|
||||
scenarios := map[int]bool{
|
||||
200: false,
|
||||
401: true,
|
||||
403: false,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
r := &Response{StatusCode: input}
|
||||
actual := r.IsNotAuthorized()
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasServerFailure(t *testing.T) {
|
||||
scenarios := map[int]bool{
|
||||
200: false,
|
||||
404: true,
|
||||
500: true,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
r := &Response{StatusCode: input}
|
||||
actual := r.HasServerFailure()
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsModifiedWith304Status(t *testing.T) {
|
||||
r := &Response{StatusCode: 304}
|
||||
if r.IsModified("etag", "lastModified") {
|
||||
t.Error("The resource should not be considered modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsModifiedWithIdenticalEtag(t *testing.T) {
|
||||
r := &Response{StatusCode: 200, ETag: "etag"}
|
||||
if r.IsModified("etag", "lastModified") {
|
||||
t.Error("The resource should not be considered modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsModifiedWithIdenticalLastModified(t *testing.T) {
|
||||
r := &Response{StatusCode: 200, LastModified: "lastModified"}
|
||||
if r.IsModified("etag", "lastModified") {
|
||||
t.Error("The resource should not be considered modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsModifiedWithDifferentHeaders(t *testing.T) {
|
||||
r := &Response{StatusCode: 200, ETag: "some etag", LastModified: "some date"}
|
||||
if !r.IsModified("etag", "lastModified") {
|
||||
t.Error("The resource should be considered modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToString(t *testing.T) {
|
||||
input := `test`
|
||||
r := &Response{Body: strings.NewReader(input)}
|
||||
|
||||
if r.BodyAsString() != input {
|
||||
t.Error(`Unexpected ouput`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureUnicodeWithHTMLDocuments(t *testing.T) {
|
||||
var unicodeTestCases = []struct {
|
||||
filename, contentType string
|
||||
convertedToUnicode bool
|
||||
}{
|
||||
{"HTTP-charset.html", "text/html; charset=iso-8859-15", true},
|
||||
{"UTF-16LE-BOM.html", "", true},
|
||||
{"UTF-16BE-BOM.html", "", true},
|
||||
{"meta-content-attribute.html", "text/html", true},
|
||||
{"meta-charset-attribute.html", "text/html", true},
|
||||
{"No-encoding-declaration.html", "text/html", true},
|
||||
{"HTTP-vs-UTF-8-BOM.html", "text/html; charset=iso-8859-15", true},
|
||||
{"HTTP-vs-meta-content.html", "text/html; charset=iso-8859-15", true},
|
||||
{"HTTP-vs-meta-charset.html", "text/html; charset=iso-8859-15", true},
|
||||
{"UTF-8-BOM-vs-meta-content.html", "text/html", true},
|
||||
{"UTF-8-BOM-vs-meta-charset.html", "text/html", true},
|
||||
{"windows_1251.html", "text/html; charset=windows-1251", true},
|
||||
{"gb2312.html", "text/html", true},
|
||||
{"urdu.xml", "text/xml; charset=utf-8", true},
|
||||
{"content-type-only-win-8859-1.xml", "application/xml; charset=ISO-8859-1", true},
|
||||
{"rdf_utf8.xml", "application/rss+xml; charset=utf-8", true},
|
||||
{"rdf_utf8.xml", "application/rss+xml; charset: utf-8", true}, // Invalid Content-Type
|
||||
{"charset-content-type-xml-iso88591.xml", "application/rss+xml; charset=ISO-8859-1", false},
|
||||
{"windows_1251.xml", "text/xml", false},
|
||||
{"smallfile.xml", "text/xml; charset=utf-8", true},
|
||||
{"single_quote_xml_encoding.xml", "text/xml; charset=utf-8", true},
|
||||
}
|
||||
|
||||
for _, tc := range unicodeTestCases {
|
||||
content, err := ioutil.ReadFile("testdata/" + tc.filename)
|
||||
if err != nil {
|
||||
t.Fatalf(`Unable to read file %q: %v`, tc.filename, err)
|
||||
}
|
||||
|
||||
r := &Response{Body: bytes.NewReader(content), ContentType: tc.contentType}
|
||||
parseErr := r.EnsureUnicodeBody()
|
||||
if parseErr != nil {
|
||||
t.Fatalf(`Unicode conversion error for %q - %q: %v`, tc.filename, tc.contentType, parseErr)
|
||||
}
|
||||
|
||||
isUnicode := utf8.ValidString(r.BodyAsString())
|
||||
if isUnicode != tc.convertedToUnicode {
|
||||
t.Errorf(`Unicode conversion %q - %q, got: %v, expected: %v`,
|
||||
tc.filename, tc.contentType, isUnicode, tc.convertedToUnicode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!-- generator="FeedCreator 1.6" -->
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
version="2.0">
|
||||
<channel>
|
||||
<title>Golem.de</title>
|
||||
<description>IT-News fuer Profis</description>
|
||||
<link>https://www.golem.de/</link>
|
||||
<atom:link rel="self" href="https://rss.golem.de/rss.php?feed=RSS2.0" />
|
||||
<lastBuildDate>Sun, 28 Oct 2018 13:49:01 +0100</lastBuildDate>
|
||||
<generator>FeedCreator 1.6</generator>
|
||||
<image>
|
||||
<url>https://www.golem.de/staticrl/images/golem-rss.png</url>
|
||||
<title>Golem.de</title>
|
||||
<link>https://www.golem.de/</link>
|
||||
<description>Golem.de News Feed</description>
|
||||
</image>
|
||||
<language>de</language>
|
||||
<atom:link rel="hub" href="http://golem.superfeedr.com/" />
|
||||
<item>
|
||||
<title>Red Dead Redemption 2: Hinweise auf PC-Umsetzung in App von Rockstar Games</title>
|
||||
<link>https://www.golem.de/news/red-dead-redemption-2-hinweise-auf-pc-umsetzung-in-app-von-rockstar-games-1810-137358-rss.html</link>
|
||||
<description>Viele Spieler wünschen sich eine PC-Version von Red Dead Redemption 2, aber Entwickler Rockstar Games schweigt zu dem Thema. Anders die offizielle Companion App: In einigen ihrer Daten gibt es Hinweise auf die Umsetzung. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/red-dead-redemption/">Red Dead Redemption</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137358&amp;page=1&amp;ts=1540730880" alt="" width="1" height="1" /></description>
|
||||
<pubDate>Sun, 28 Oct 2018 13:48:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137358-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137358-177541-177538_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Viele Spieler wünschen sich eine PC-Version von Red Dead Redemption 2, aber Entwickler Rockstar Games schweigt zu dem Thema. Anders die offizielle Companion App: In einigen ihrer Daten gibt es Hinweise auf die Umsetzung. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/red-dead-redemption/">Red Dead Redemption</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137358&page=1&ts=1540730880" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments />
|
||||
</item>
|
||||
<item>
|
||||
<title>Let's Play: Twitch will Streamer zusammen spielen und singen lassen</title>
|
||||
<link>https://www.golem.de/news/let-s-play-twitch-will-streamer-zusammen-spielen-und-singen-lassen-1810-137357-rss.html</link>
|
||||
<description>Der Streamingdienst Twitch hat auf seiner Hausmesse neue Funktionen für Kanalbetreiber und Zuschauer vorgestellt. Unter anderem soll es künftig Übertragungen mit bis zu vier Spielern geben - und Singwettbewerbe. (<a href="https://www.golem.de/specials/twitch/">Twitch</a>, <a href="https://www.golem.de/specials/amazon/">Amazon</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137357&amp;page=1&amp;ts=1540728000" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/games/let-s-play-twitch-will-streamer-zusammen-spielen-und-singen-lassen/121579,list.html</comments>
|
||||
<pubDate>Sun, 28 Oct 2018 13:00:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137357-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137357-177536-177533_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Streamingdienst Twitch hat auf seiner Hausmesse neue Funktionen für Kanalbetreiber und Zuschauer vorgestellt. Unter anderem soll es künftig Übertragungen mit bis zu vier Spielern geben - und Singwettbewerbe. (<a href="https://www.golem.de/specials/twitch/">Twitch</a>, <a href="https://www.golem.de/specials/amazon/">Amazon</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137357&page=1&ts=1540728000" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments />
|
||||
</item>
|
||||
<item>
|
||||
<title>Zhuque-1: Erste private chinesische Satellitenmission fehlgeschlagen</title>
|
||||
<link>https://www.golem.de/news/zhuque-1-erste-private-chinesische-satellitenmission-fehlgeschlagen-1810-137356-rss.html</link>
|
||||
<description>Die Zhuque-1 hat es nicht in den Orbit geschafft: Beim Zünden der dritten Raketenstufe kam es zu Problemen. Bei einem Erfolg wäre der Hersteller Landspace das erste von rund 60 kommerziellen chinesischen Unternehmen gewesen, das einen Satelliten ins All gebracht hätte. (<a href="https://www.golem.de/specials/raumfahrt/">Raumfahrt</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137356&amp;page=1&amp;ts=1540722420" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/internet/zhuque-1-erste-private-chinesische-satellitenmission-fehlgeschlagen/121578,list.html</comments>
|
||||
<pubDate>Sun, 28 Oct 2018 11:27:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137356-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137356-177532-177529_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Zhuque-1 hat es nicht in den Orbit geschafft: Beim Zünden der dritten Raketenstufe kam es zu Problemen. Bei einem Erfolg wäre der Hersteller Landspace das erste von rund 60 kommerziellen chinesischen Unternehmen gewesen, das einen Satelliten ins All gebracht hätte. (<a href="https://www.golem.de/specials/raumfahrt/">Raumfahrt</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137356&page=1&ts=1540722420" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>1</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>City Transformer: Startup entwickelt faltbares Elektroauto gegen Parkplatznot</title>
|
||||
<link>https://www.golem.de/news/city-transformer-startup-entwickelt-faltbares-elektroauto-gegen-parkplatznot-1810-137355-rss.html</link>
|
||||
<description>Es passt fast in jede Parklücke: Ein Faltauto des Startups City Transformer soll Städtern künftig das Leben erleichtern. Das innovative Fahrzeug wird zusammen mit Yamaha entwickelt. Vorbestellungen sollen voraussichtlich ab 2020 möglich sein, mehrere Versionen sind geplant. (<a href="https://www.golem.de/specials/elektromobilitaet/">Elektromobilität</a>, <a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137355&amp;page=1&amp;ts=1540721400" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/automobil/city-transformer-startup-entwickelt-faltbares-elektroauto-gegen-parkplatznot/121577,list.html</comments>
|
||||
<pubDate>Sun, 28 Oct 2018 11:10:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137355-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137355-177527-177524_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Es passt fast in jede Parklücke: Ein Faltauto des Startups City Transformer soll Städtern künftig das Leben erleichtern. Das innovative Fahrzeug wird zusammen mit Yamaha entwickelt. Vorbestellungen sollen voraussichtlich ab 2020 möglich sein, mehrere Versionen sind geplant. (<a href="https://www.golem.de/specials/elektromobilitaet/">Elektromobilität</a>, <a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137355&page=1&ts=1540721400" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>37</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Machine Learning: Von KI erstelltes Porträt für 432.500 US.Dollar versteigert</title>
|
||||
<link>https://www.golem.de/news/machine-learning-von-ki-erstelltes-portraet-fuer-432-500-us-dollar-versteigert-1810-137353-rss.html</link>
|
||||
<description>Kann Software Kunst erstellen? Eine erfolgreiche Auktion beweist, dass es zumindest Abnehmer dafür gibt. Allerdings hat sich das Entwicklerteam Obvious wohl stark bei anderen KI-Systemen bedient. (<a href="https://www.golem.de/specials/neuronalesnetzwerk/">Neuronales Netzwerk</a>, <a href="https://www.golem.de/specials/ki/">KI</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137353&amp;page=1&amp;ts=1540643580" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/applikationen/machine-learning-von-ki-erstelltes-portraet-fuer-432.500-us.dollar-versteigert/121575,list.html</comments>
|
||||
<pubDate>Sat, 27 Oct 2018 13:33:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137353-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137353-177523-177520_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Kann Software Kunst erstellen? Eine erfolgreiche Auktion beweist, dass es zumindest Abnehmer dafür gibt. Allerdings hat sich das Entwicklerteam Obvious wohl stark bei anderen KI-Systemen bedient. (<a href="https://www.golem.de/specials/neuronalesnetzwerk/">Neuronales Netzwerk</a>, <a href="https://www.golem.de/specials/ki/">KI</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137353&page=1&ts=1540643580" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>31</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Projekt Jedi: Microsoft will weiter mit US-Militär zusammenarbeiten</title>
|
||||
<link>https://www.golem.de/news/project-jedi-microsoft-will-weiter-mit-us-militaer-zusammenarbeiten-1810-137352-rss.html</link>
|
||||
<description>In einem Blogbeitrag hat sich Microsoft-Präsident Brad Smith zur Zusammenarbeit mit dem US-Verteidigungsministerium bekannt. Mitarbeiter, die nicht an derartigen Projekten arbeiten wollen, sollen in andere Bereiche des Unternehmens wechseln können. (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/google/">Google</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137352&amp;page=1&amp;ts=1540641780" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/internet/projekt-jedi-microsoft-will-weiter-mit-us-militaer-zusammenarbeiten/121574,list.html</comments>
|
||||
<pubDate>Sat, 27 Oct 2018 13:03:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137352-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137055-176130-176127_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">In einem Blogbeitrag hat sich Microsoft-Präsident Brad Smith zur Zusammenarbeit mit dem US-Verteidigungsministerium bekannt. Mitarbeiter, die nicht an derartigen Projekten arbeiten wollen, sollen in andere Bereiche des Unternehmens wechseln können. (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/google/">Google</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137352&page=1&ts=1540641780" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>21</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Star Wars: Boba-Fett-Film ist "zu 100 Prozent tot"</title>
|
||||
<link>https://www.golem.de/news/star-wars-boba-fett-film-ist-zu-100-prozent-tot-1810-137351-rss.html</link>
|
||||
<description>Es wird wohl doch keinen dritten Star-Wars-Ableger geben, der sich um den kultigen Kopfgeldjäger Boba Fett dreht. Das wird laut einem Medienbericht teils auf den geringen Erfolg des Han-Solo-Films zurückgeführt. Stattdessen soll ein bisher unbekannter Charakter in einer Serie die mandalorianische Rüstung anziehen. (<a href="https://www.golem.de/specials/star-wars/">Star Wars</a>, <a href="https://www.golem.de/specials/film/">Film</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137351&amp;page=1&amp;ts=1540639620" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/audio-video/star-wars-boba-fett-film-ist-zu-100-prozent-tot/121573,list.html</comments>
|
||||
<pubDate>Sat, 27 Oct 2018 12:27:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137351-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137351-177519-177514_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Es wird wohl doch keinen dritten Star-Wars-Ableger geben, der sich um den kultigen Kopfgeldjäger Boba Fett dreht. Das wird laut einem Medienbericht teils auf den geringen Erfolg des Han-Solo-Films zurückgeführt. Stattdessen soll ein bisher unbekannter Charakter in einer Serie die mandalorianische Rüstung anziehen. (<a href="https://www.golem.de/specials/star-wars/">Star Wars</a>, <a href="https://www.golem.de/specials/film/">Film</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137351&page=1&ts=1540639620" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>148</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Lenovo: Fehlerhafte Bios-Einstellung macht Thinkpads unbrauchbar</title>
|
||||
<link>https://www.golem.de/news/lenovo-fehlerhafte-bios-einstellung-macht-thinkpads-unbrauchbar-1810-137350-rss.html</link>
|
||||
<description>Die Bios-Unterstützung für Thunderbolt bei Thinkpads zu aktivieren, ist derzeit keine gute Idee: Mehrere Nutzer berichten von nicht mehr startenden Notebooks, nachdem sie diese Funktion aktiviert haben. Das konnte auf diversen Linux-Distributionen, aber auch mit Windows 10 repliziert werden. (<a href="https://www.golem.de/specials/lenovo/">Lenovo</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137350&amp;page=1&amp;ts=1540633200" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/applikationen/lenovo-fehlerhafte-bios-einstellung-macht-thinkpads-unbrauchbar/121572,list.html</comments>
|
||||
<pubDate>Sat, 27 Oct 2018 10:40:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137350-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137350-177513-177510_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Bios-Unterstützung für Thunderbolt bei Thinkpads zu aktivieren, ist derzeit keine gute Idee: Mehrere Nutzer berichten von nicht mehr startenden Notebooks, nachdem sie diese Funktion aktiviert haben. Das konnte auf diversen Linux-Distributionen, aber auch mit Windows 10 repliziert werden. (<a href="https://www.golem.de/specials/lenovo/">Lenovo</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137350&page=1&ts=1540633200" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>16</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Wochenrückblick: Wilder Westen, buntes Handy, nutzloses Siegel</title>
|
||||
<link>https://www.golem.de/news/wochenrueckblick-wilder-westen-buntes-handy-nutzloses-siegel-1810-137318-rss.html</link>
|
||||
<description> Wir testen das iPhone Xr, sind ein Revolverheld und entdecken wieder Sicherheitslücken. Sieben Tage und viele Meldungen im Überblick. (<a href="https://www.golem.de/specials/golemwochenrueckblick/">Golem-Wochenrückblick</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137318&amp;page=1&amp;ts=1540623720" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/politik-recht/wochenrueckblick-wilder-westen-buntes-handy-nutzloses-siegel/121571,list.html</comments>
|
||||
<pubDate>Sat, 27 Oct 2018 08:02:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137318-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137318-177504-177501_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Wir testen das iPhone Xr, sind ein Revolverheld und entdecken wieder Sicherheitslücken. Sieben Tage und viele Meldungen im Überblick. (<a href="https://www.golem.de/specials/golemwochenrueckblick/">Golem-Wochenrückblick</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137318&page=1&ts=1540623720" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments />
|
||||
</item>
|
||||
<item>
|
||||
<title>Fernsehen: 5G-Netz wird so wichtig wie Strom und Wasser</title>
|
||||
<link>https://www.golem.de/news/fernsehen-5g-netz-wird-so-wichtig-wie-strom-und-wasser-1810-137349-rss.html</link>
|
||||
<description>Ein 5G-FeMBMS-Sendernetz für die Fernsehverbreitung sorgt für Aufsehen, noch bevor man weiß, ob es funktioniert. Wie Rundfunkübertragung und Mobilfunk zusammenkommen können, wurde auf den Medientagen München besprochen. (<a href="https://www.golem.de/specials/fernsehen/">Fernsehen</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137349&amp;page=1&amp;ts=1540572960" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/handy/fernsehen-5g-netz-wird-so-wichtig-wie-strom-und-wasser/121570,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 17:56:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137349-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137349-177509-177506_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ein 5G-FeMBMS-Sendernetz für die Fernsehverbreitung sorgt für Aufsehen, noch bevor man weiß, ob es funktioniert. Wie Rundfunkübertragung und Mobilfunk zusammenkommen können, wurde auf den Medientagen München besprochen. (<a href="https://www.golem.de/specials/fernsehen/">Fernsehen</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137349&page=1&ts=1540572960" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>25</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Linux und BSD: Sicherheitslücke in X.org ermöglicht Root-Rechte</title>
|
||||
<link>https://www.golem.de/news/linux-und-bsd-sicherheitsluecke-in-x-org-ermoeglicht-root-rechte-1810-137347-rss.html</link>
|
||||
<description>Eine Sicherheitslücke im Displayserver X.org erlaubt unter bestimmten Umständen das Überschreiben von Dateien und das Ausweiten der Benutzerrechte. Der passende Exploit passt in einen Tweet. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/openbsd/">OpenBSD</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137347&amp;page=1&amp;ts=1540564620" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/linux-und-bsd-sicherheitsluecke-in-x.org-ermoeglicht-root-rechte/121569,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 15:37:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137347-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137347-177500-177497_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Eine Sicherheitslücke im Displayserver X.org erlaubt unter bestimmten Umständen das Überschreiben von Dateien und das Ausweiten der Benutzerrechte. Der passende Exploit passt in einen Tweet. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/openbsd/">OpenBSD</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137347&page=1&ts=1540564620" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>47</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Augsburg: Fujitsu Deutschland macht alles dicht</title>
|
||||
<link>https://www.golem.de/news/augsburg-fujitsu-deutschland-macht-alles-dicht-1810-137348-rss.html</link>
|
||||
<description>Fujitsu will seine gesamte Fertigung außerhalb Japans schließen. In Deutschland ist der Standort in Augsburg komplett betroffen. (<a href="https://www.golem.de/specials/fujitsu/">Fujitsu</a>, <a href="https://www.golem.de/specials/sap/">SAP</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137348&amp;page=1&amp;ts=1540562340" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/augsburg-fujitsu-deutschland-macht-alles-dicht/121568,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 14:59:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137348-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137348-177485-177484_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Fujitsu will seine gesamte Fertigung außerhalb Japans schließen. In Deutschland ist der Standort in Augsburg komplett betroffen. (<a href="https://www.golem.de/specials/fujitsu/">Fujitsu</a>, <a href="https://www.golem.de/specials/sap/">SAP</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137348&page=1&ts=1540562340" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>56</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Bundesnetzagentur: Seehofer fordert Verschiebung von 5G-Auktion</title>
|
||||
<link>https://www.golem.de/news/bundesnetzagentur-seehofer-fordert-verschiebung-von-5g-auktion-1810-137346-rss.html</link>
|
||||
<description>Bundesinnenminister Horst Seehofer will die 5G-Auktion verschieben, bis die ländlichen Regionen besser berücksichtigt werden. Er wird von einer Gruppe um den CDU-Abgeordneten Stefan Rouenhoff unterstützt. (<a href="https://www.golem.de/specials/5g/">5G</a>, <a href="https://www.golem.de/specials/bundesnetzagentur/">Bundesnetzagentur</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137346&amp;page=1&amp;ts=1540557900" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/handy/bundesnetzagentur-seehofer-fordert-verschiebung-von-5g-auktion/121567,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 13:45:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137346-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137346-177483-177480_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Bundesinnenminister Horst Seehofer will die 5G-Auktion verschieben, bis die ländlichen Regionen besser berücksichtigt werden. Er wird von einer Gruppe um den CDU-Abgeordneten Stefan Rouenhoff unterstützt. (<a href="https://www.golem.de/specials/5g/">5G</a>, <a href="https://www.golem.de/specials/bundesnetzagentur/">Bundesnetzagentur</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137346&page=1&ts=1540557900" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>14</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Linux und Patente: Open Source bei Microsoft ist "Kultur statt Strategie"</title>
|
||||
<link>https://www.golem.de/news/linux-und-patente-open-source-bei-microsoft-ist-kultur-statt-strategie-1810-137345-rss.html</link>
|
||||
<description>Der Microsoft-Angestellte Stephen Walli beschreibt den Wandel bei Microsoft hin zu Open Source Software und Linux als kulturell getrieben. Mit Blick auf den Beitritt zu dem Patentpool des Open Invention Network zeigt sich jedoch auch, dass das Unternehmen noch sehr viel Arbeit vor sich hat. Ein Bericht von Sebastian Grüner (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/softwarepatente/">Softwarepatent</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137345&amp;page=1&amp;ts=1540556820" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/politik-recht/linux-und-patente-open-source-bei-microsoft-ist-kultur-statt-strategie/121566,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 13:27:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137345-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137345-177479-177475_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Microsoft-Angestellte Stephen Walli beschreibt den Wandel bei Microsoft hin zu Open Source Software und Linux als kulturell getrieben. Mit Blick auf den Beitritt zu dem Patentpool des Open Invention Network zeigt sich jedoch auch, dass das Unternehmen noch sehr viel Arbeit vor sich hat. Ein Bericht von Sebastian Grüner (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/softwarepatente/">Softwarepatent</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137345&page=1&ts=1540556820" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>20</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Sicherheitslücke: Daten von 185.000 weiteren British-Airways-Kunden betroffen</title>
|
||||
<link>https://www.golem.de/news/sicherheitsluecke-daten-von-185-000-weiteren-british-airways-kunden-betroffen-1810-137344-rss.html</link>
|
||||
<description>Von dem Datenleck im Buchungssystem von British Airways waren deutlich mehr Kunden betroffen als bisher bekannt. Die Fluggesellschaft rät betroffenen Kunden, ihre Bank zu kontaktieren. Kreditkarten werden in diesem Fall häufig komplett ausgetauscht. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137344&amp;page=1&amp;ts=1540553820" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/sicherheitsluecke-daten-von-185.000-weiteren-british-airways-kunden-betroffen/121565,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 12:37:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137344-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137344-177474-177471_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Von dem Datenleck im Buchungssystem von British Airways waren deutlich mehr Kunden betroffen als bisher bekannt. Die Fluggesellschaft rät betroffenen Kunden, ihre Bank zu kontaktieren. Kreditkarten werden in diesem Fall häufig komplett ausgetauscht. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137344&page=1&ts=1540553820" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments />
|
||||
</item>
|
||||
<item>
|
||||
<title>iPhone Xr im Test: Apples günstigeres iPhone ist nicht günstig</title>
|
||||
<link>https://www.golem.de/news/iphone-xr-im-test-apples-guenstiges-iphone-ist-nicht-guenstig-1810-137327-rss.html</link>
|
||||
<description>Apple versucht es 2018 wieder einmal mit einem relativ preisgünstigen iPhone - weniger teuer als die Xs-Modelle, aber mit 850 Euro auch nicht gerade preiswert. Käufer bekommen dafür allerdings auch ein Smartphone mit sehr guter Ausstattung, in einigen Punkten wurde jedoch auf Hardware der teuren Modelle verzichtet. Ein Test von Tobias Költzsch (<a href="https://www.golem.de/specials/iphone/">iPhone</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137327&amp;page=1&amp;ts=1540548180" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/handy/iphone-xr-im-test-apples-guenstigeres-iphone-ist-nicht-guenstig/121563,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 11:03:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137327-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137327-177418-177414_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Apple versucht es 2018 wieder einmal mit einem relativ preisgünstigen iPhone - weniger teuer als die Xs-Modelle, aber mit 850 Euro auch nicht gerade preiswert. Käufer bekommen dafür allerdings auch ein Smartphone mit sehr guter Ausstattung, in einigen Punkten wurde jedoch auf Hardware der teuren Modelle verzichtet. Ein Test von Tobias Költzsch (<a href="https://www.golem.de/specials/iphone/">iPhone</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137327&page=1&ts=1540548180" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>169</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Microsoft: PC-Spieleangebot des Xbox Game Pass wird erweitert</title>
|
||||
<link>https://www.golem.de/news/microsoft-pc-spieleangebot-des-xbox-game-pass-wird-erweitert-1810-137343-rss.html</link>
|
||||
<description>Der Xbox Game Pass soll künftig um mehr Angebote für Windows-PC erweitert werden, sagt Microsoft-Chef Satya Nadella. Derzeit gibt es für 10 Euro nur wenige plattformübergreifend verfügbare Spiele. (<a href="https://www.golem.de/specials/xbox-one/">Xbox One</a>, <a href="https://www.golem.de/specials/microsoft/">Microsoft</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137343&amp;page=1&amp;ts=1540546800" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/microsoft-pc-spieleangebot-des-xbox-game-pass-wird-erweitert/121562,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 10:40:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137343-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137343-177453-177450_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Xbox Game Pass soll künftig um mehr Angebote für Windows-PC erweitert werden, sagt Microsoft-Chef Satya Nadella. Derzeit gibt es für 10 Euro nur wenige plattformübergreifend verfügbare Spiele. (<a href="https://www.golem.de/specials/xbox-one/">Xbox One</a>, <a href="https://www.golem.de/specials/microsoft/">Microsoft</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137343&page=1&ts=1540546800" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>19</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Breitbandgesellschaft: Grüne wollen Netzbetreiber zum Ausbau zwingen</title>
|
||||
<link>https://www.golem.de/news/breitbandgesellschaft-gruene-wollen-netzbetreiber-zum-ausbau-zwingen-1810-137342-rss.html</link>
|
||||
<description>Die Grünen haben die Netzversorgung in Deutschland analysiert. Die Partei, die zurzeit in Wählerumfragen stark zugewinnt, fordert den Breitbandausbau auf Kosten der Konzerne und will Glasfaser staatlich durchsetzen. (<a href="https://www.golem.de/specials/breitband/">Breitband</a>, <a href="https://www.golem.de/specials/handy/">Handy</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137342&amp;page=1&amp;ts=1540545840" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/politik-recht/breitbandgesellschaft-gruene-wollen-netzbetreiber-zum-ausbau-zwingen/121561,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 10:24:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137342-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1807/135605-169300-169297_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Grünen haben die Netzversorgung in Deutschland analysiert. Die Partei, die zurzeit in Wählerumfragen stark zugewinnt, fordert den Breitbandausbau auf Kosten der Konzerne und will Glasfaser staatlich durchsetzen. (<a href="https://www.golem.de/specials/breitband/">Breitband</a>, <a href="https://www.golem.de/specials/handy/">Handy</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137342&page=1&ts=1540545840" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>22</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Studie: Silicon Valley dient als rechte Hand des großen Bruders</title>
|
||||
<link>https://www.golem.de/news/studie-silicon-valley-dient-als-rechte-hand-des-grossen-bruders-1810-137316-rss.html</link>
|
||||
<description>Die US-Hightech-Branche verdingt sich zunehmend als technischer Dienstleister für staatliche Big-Brother-Projekte wie die Überwachung und Abschiebung von Immigranten, heißt es in einem Bericht von Bürgerrechtlern. Amazon und Palantir verdienten damit am meisten. Von Stefan Krempl (<a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>, <a href="https://www.golem.de/specials/ibm/">IBM</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137316&amp;page=1&amp;ts=1540543080" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/studie-silicon-valley-dient-als-rechte-hand-des-grossen-bruders/121560,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 09:38:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137316-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137316-177360-177357_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die US-Hightech-Branche verdingt sich zunehmend als technischer Dienstleister für staatliche Big-Brother-Projekte wie die Überwachung und Abschiebung von Immigranten, heißt es in einem Bericht von Bürgerrechtlern. Amazon und Palantir verdienten damit am meisten. Von Stefan Krempl (<a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>, <a href="https://www.golem.de/specials/ibm/">IBM</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137316&page=1&ts=1540543080" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>20</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Bethesda: Postnukleare PC-Systemanforderungen für Fallout 76</title>
|
||||
<link>https://www.golem.de/news/bethesda-postnukleare-pc-systemanforderungen-fuer-fallout-76-1810-137340-rss.html</link>
|
||||
<description>Kurz vor dem Start der Betaversion für PC-Spieler hat Bethesda die Systemanforderung von Fallout 76 veröffentlicht. Hoffnungen auf lange Abenteuer in der Vorabversion gibt es aber zumindest nach den Erfahrungen des Xbox-Zugangs eher nicht. (<a href="https://www.golem.de/specials/fallout-76/">Fallout 76</a>, <a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137340&amp;page=1&amp;ts=1540542180" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/games/bethesda-postnukleare-pc-systemanforderungen-fuer-fallout-76/121559,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 09:23:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137340-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137340-177448-177445_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Kurz vor dem Start der Betaversion für PC-Spieler hat Bethesda die Systemanforderung von Fallout 76 veröffentlicht. Hoffnungen auf lange Abenteuer in der Vorabversion gibt es aber zumindest nach den Erfahrungen des Xbox-Zugangs eher nicht. (<a href="https://www.golem.de/specials/fallout-76/">Fallout 76</a>, <a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137340&page=1&ts=1540542180" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>101</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Quartalszahlen: Intel legt 19-Milliarden-USD-Rekord vor</title>
|
||||
<link>https://www.golem.de/news/quartalszahlen-intel-legt-19-milliarden-usd-rekord-vor-1810-137339-rss.html</link>
|
||||
<description>Ungeachtet der 14-nm-Knappheit und diverser Sicherheitslücken konnte Intel im dritten Quartal 2018 mehr Umsatz erwirtschaften und mehr Gewinn erzielen als jemals zuvor. Vor allem das florierende Server-Geschäft wird bei Intel immer wichtiger. (<a href="https://www.golem.de/specials/intel/">Intel</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137339&amp;page=1&amp;ts=1540540260" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalszahlen-intel-legt-19-milliarden-usd-rekord-vor/121558,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 08:51:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137339-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137339-177444-177441_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ungeachtet der 14-nm-Knappheit und diverser Sicherheitslücken konnte Intel im dritten Quartal 2018 mehr Umsatz erwirtschaften und mehr Gewinn erzielen als jemals zuvor. Vor allem das florierende Server-Geschäft wird bei Intel immer wichtiger. (<a href="https://www.golem.de/specials/intel/">Intel</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137339&page=1&ts=1540540260" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>10</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Physik: Weg mit der Schönheit!</title>
|
||||
<link>https://www.golem.de/news/physik-weg-mit-der-schoenheit-1810-137161-rss.html</link>
|
||||
<description>Ist eine Theorie richtig, nur weil sie schön ist? Nein, sagt Sabine Hossenfelder. In ihrem Buch "Das hässliche Universum" zeigt die theoretische Physikerin, wie das Schönheitsdenken die Wissenschaft lähmt und erklärt dabei recht unterhaltsam die unterschiedlichen Theorien und Modelle der Teilchenphysik. Eine Rezension von Friedemann Zweynert (<a href="https://www.golem.de/specials/physik/">Physik</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137161&amp;page=1&amp;ts=1540537380" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/internet/physik-weg-mit-der-schoenheit/121557,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 08:03:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137161-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137161-176716-176713_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ist eine Theorie richtig, nur weil sie schön ist? Nein, sagt Sabine Hossenfelder. In ihrem Buch "Das hässliche Universum" zeigt die theoretische Physikerin, wie das Schönheitsdenken die Wissenschaft lähmt und erklärt dabei recht unterhaltsam die unterschiedlichen Theorien und Modelle der Teilchenphysik. Eine Rezension von Friedemann Zweynert (<a href="https://www.golem.de/specials/physik/">Physik</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137161&page=1&ts=1540537380" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>86</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Elon Musk: Teslas Model 3 für 35.000 US-Dollar derzeit unmöglich</title>
|
||||
<link>https://www.golem.de/news/elon-musk-teslas-model-3-fuer-35-000-us-dollar-derzeit-unmoeglich-1810-137335-rss.html</link>
|
||||
<description>Tesla-Chef Elon Musk hat eingeräumt, das bei 35.000 US-Dollar startende Basismodell des Elektroautos Model 3 immer noch nicht liefern zu können. (<a href="https://www.golem.de/specials/tesla-model-3/">Tesla Model 3</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137335&amp;page=1&amp;ts=1540533540" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/automobil/elon-musk-teslas-model-3-fuer-35.000-us-dollar-derzeit-unmoeglich/121556,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 06:59:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137335-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137335-177428-177424_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Tesla-Chef Elon Musk hat eingeräumt, das bei 35.000 US-Dollar startende Basismodell des Elektroautos Model 3 immer noch nicht liefern zu können. (<a href="https://www.golem.de/specials/tesla-model-3/">Tesla Model 3</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137335&page=1&ts=1540533540" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>235</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Solarzellen als Dach: Tesla-Solarschindeln verzögern sich bis 2019</title>
|
||||
<link>https://www.golem.de/news/solarzellen-als-dach-tesla-solarschindeln-verzoegern-sich-auf-2019-1810-137334-rss.html</link>
|
||||
<description>Tesla wird die Serienproduktion seiner Solardachziegel nicht mehr wie geplant in diesem Jahr starten. Nach Angaben von Firmenchef Elon Musk verschiebt sich das Vorhaben auf 2019. (<a href="https://www.golem.de/specials/solarenergie/">Solarenergie</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137334&amp;page=1&amp;ts=1540532460" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wissenschaft/solarzellen-als-dach-tesla-solarschindeln-verzoegern-sich-bis-2019/121555,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 06:41:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137334-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1705/127761-139613-i_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Tesla wird die Serienproduktion seiner Solardachziegel nicht mehr wie geplant in diesem Jahr starten. Nach Angaben von Firmenchef Elon Musk verschiebt sich das Vorhaben auf 2019. (<a href="https://www.golem.de/specials/solarenergie/">Solarenergie</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137334&page=1&ts=1540532460" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>47</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Uniti One: Elektroauto für 15.000 Euro wird in Großbritannien gebaut</title>
|
||||
<link>https://www.golem.de/news/uniti-one-elektroauto-fuer-15-000-euro-wird-in-grossbritannien-gebaut-1810-137333-rss.html</link>
|
||||
<description>Das schwedische Unternehmen Uniti will sein Elektroauto One in Großbritannien bauen. Einen fahrenden Prototyp des Uniti One gibt es schon. Das Auto soll je nach Modell für 15.000 bis 20.000 Euro auf den Markt kommen. (<a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137333&amp;page=1&amp;ts=1540531080" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/automobil/uniti-one-elektroauto-fuer-15.000-euro-wird-in-grossbritannien-gebaut/121554,list.html</comments>
|
||||
<pubDate>Fri, 26 Oct 2018 06:18:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137333-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1805/134432-163131-163128_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Das schwedische Unternehmen Uniti will sein Elektroauto One in Großbritannien bauen. Einen fahrenden Prototyp des Uniti One gibt es schon. Das Auto soll je nach Modell für 15.000 bis 20.000 Euro auf den Markt kommen. (<a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137333&page=1&ts=1540531080" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>28</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Quartalsbericht: Alphabet macht in drei Monaten 9,2 Milliarden Dollar Gewinn</title>
|
||||
<link>https://www.golem.de/news/quartalsbericht-alphabet-macht-in-drei-monaten-9-2-milliarden-dollar-gewinn-1810-137337-rss.html</link>
|
||||
<description>Alphabet erwirtschaftet weiter extrem hohe Gewinne, der Umsatz wächst nicht ganz so stark. Google muss zugleich auf eine Enthüllung in der US-Presse zu sexueller Belästigung um Android-Begründer Andy Rubin reagieren. (<a href="https://www.golem.de/specials/google/">Google</a>, <a href="https://www.golem.de/specials/boerse/">Börse</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137337&amp;page=1&amp;ts=1540504320" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalsbericht-alphabet-macht-in-drei-monaten-9-2-milliarden-dollar-gewinn/121553,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 22:52:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137337-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137053-176114-176111_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Alphabet erwirtschaftet weiter extrem hohe Gewinne, der Umsatz wächst nicht ganz so stark. Google muss zugleich auf eine Enthüllung in der US-Presse zu sexueller Belästigung um Android-Begründer Andy Rubin reagieren. (<a href="https://www.golem.de/specials/google/">Google</a>, <a href="https://www.golem.de/specials/boerse/">Börse</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137337&page=1&ts=1540504320" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>12</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Quartalsbericht: Amazon verfehlt die Umsatzprognosen</title>
|
||||
<link>https://www.golem.de/news/quartalsbericht-amazon-verfehlt-die-umsatzprognosen-1810-137336-rss.html</link>
|
||||
<description>Amazon weist erneut einen hohen Gewinn aus. Doch der Konzern lag beim Umsatz unter den Prognosen der Analysten. (<a href="https://www.golem.de/specials/amazon/">Amazon</a>, <a href="https://www.golem.de/specials/onlineshop/">Onlineshop</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137336&amp;page=1&amp;ts=1540500780" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalsbericht-amazon-verfehlt-die-umsatzprognosen/121552,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 21:53:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137336-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137336-177432-177429_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Amazon weist erneut einen hohen Gewinn aus. Doch der Konzern lag beim Umsatz unter den Prognosen der Analysten. (<a href="https://www.golem.de/specials/amazon/">Amazon</a>, <a href="https://www.golem.de/specials/onlineshop/">Onlineshop</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137336&page=1&ts=1540500780" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>24</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Datenskandal: Britische Datenschutzbehörde verurteilt Facebook</title>
|
||||
<link>https://www.golem.de/news/datenskandal-britische-datenschutzbehoerde-verurteilt-facebook-1810-137332-rss.html</link>
|
||||
<description>Im Skandal um Cambridge Analytica hat die britische Datenschutzbehörde die Höchststrafe von 500.000 Pfund verhängt. Facebook habe einen schweren Verstoß gegen geltendes Recht zugelassen. (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137332&amp;page=1&amp;ts=1540483080" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/datenskandal-britische-datenschutzbehoerde-verurteilt-facebook/121550,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 16:58:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137332-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137332-177420-177419_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Im Skandal um Cambridge Analytica hat die britische Datenschutzbehörde die Höchststrafe von 500.000 Pfund verhängt. Facebook habe einen schweren Verstoß gegen geltendes Recht zugelassen. (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137332&page=1&ts=1540483080" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>35</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Corsair: Neue K70 MK.2 kommt mit Cherrys Low-Profile-Switches</title>
|
||||
<link>https://www.golem.de/news/corsair-neue-k70-mk-2-kommt-mit-cherrys-low-profile-switches-1810-137331-rss.html</link>
|
||||
<description>Corsair erweitert sein Tastaturportefeuille um zwei Gaming-Tastaturen mit Cherrys flachen Low-Profile-Switches. Ein Modell hat Schalter mit einem besonders kurzem Auslöseweg von 1 mm - die Schalter darf Corsair exklusiv verwenden. (<a href="https://www.golem.de/specials/corsair/">Corsair</a>, <a href="https://www.golem.de/specials/eingabegeraet/">Eingabegerät</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137331&amp;page=1&amp;ts=1540480320" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/sonstiges/corsair-neue-k70-mk.2-kommt-mit-cherrys-low-profile-switches/121549,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 16:12:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137331-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137331-177413-177410_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Corsair erweitert sein Tastaturportefeuille um zwei Gaming-Tastaturen mit Cherrys flachen Low-Profile-Switches. Ein Modell hat Schalter mit einem besonders kurzem Auslöseweg von 1 mm - die Schalter darf Corsair exklusiv verwenden. (<a href="https://www.golem.de/specials/corsair/">Corsair</a>, <a href="https://www.golem.de/specials/eingabegeraet/">Eingabegerät</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137331&page=1&ts=1540480320" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>19</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Cambridge-Analytica-Skandal: EU-Parlament fordert schärfere Kontrolle von Facebook</title>
|
||||
<link>https://www.golem.de/news/cambridge-analytica-skandal-eu-parlament-fordert-schaerfere-kontrolle-von-facebook-1810-137329-rss.html</link>
|
||||
<description>Die EU-Abgeordneten haben als Reaktion auf die Datenschutzverstöße von Facebook und Cambridge Analytica Behörden angehalten, ihre Aktivitäten auf dem Netzwerk zu überdenken. Profiling zu politischen Zwecken wollen sie verbieten. Von Stefan Krempl (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137329&amp;page=1&amp;ts=1540479120" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/cambridge-analytica-skandal-eu-parlament-fordert-schaerfere-kontrolle-von-facebook/121548,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 15:52:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137329-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137329-177404-177403_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die EU-Abgeordneten haben als Reaktion auf die Datenschutzverstöße von Facebook und Cambridge Analytica Behörden angehalten, ihre Aktivitäten auf dem Netzwerk zu überdenken. Profiling zu politischen Zwecken wollen sie verbieten. Von Stefan Krempl (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137329&page=1&ts=1540479120" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>3</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Kuriosum: Das Hellgate London öffnet sich mal wieder</title>
|
||||
<link>https://www.golem.de/news/kuriosum-das-hellgate-london-oeffnet-sich-mal-wieder-1810-137328-rss.html</link>
|
||||
<description>Einer der großen Trash-Klassiker der Spielegeschichte wagt einen neuen Anlauf: Mitte November 2018 soll ein neue, für Einzelspieler ausgelegte Windows-Fassung von Hellgate London erscheinen. Das Ursprungskonzept hatten sich ehemalige Blizzard-Chefentwickler ausgedacht. (<a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>, <a href="https://www.golem.de/specials/steam/">Steam</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137328&amp;page=1&amp;ts=1540476600" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/games/kuriosum-das-hellgate-london-oeffnet-sich-mal-wieder/121547,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 15:10:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137328-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137328-177396-177392_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Einer der großen Trash-Klassiker der Spielegeschichte wagt einen neuen Anlauf: Mitte November 2018 soll ein neue, für Einzelspieler ausgelegte Windows-Fassung von Hellgate London erscheinen. Das Ursprungskonzept hatten sich ehemalige Blizzard-Chefentwickler ausgedacht. (<a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>, <a href="https://www.golem.de/specials/steam/">Steam</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137328&page=1&ts=1540476600" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>61</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Tweether: 10 GBit/s über einen Quadratkilometer verteilt</title>
|
||||
<link>https://www.golem.de/news/tweether-10-gbit-s-ueber-einen-quadratkilometer-verteilt-1810-137326-rss.html</link>
|
||||
<description>Eine neue Technologie verteilt 10 GBit/s über eine große Fläche. Der erste Feldversuch ist erfolgreich verlaufen. Zum ersten Mal wurde ein stabiles drahtloses Netzwerk bei diesen Frequenzen und mit diesen Datenraten betrieben. (<a href="https://www.golem.de/specials/wissenschaft/">Wissenschaft</a>, <a href="https://www.golem.de/specials/mobil/">Mobil</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137326&amp;page=1&amp;ts=1540475400" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wissenschaft/tweether-10-gbit-s-ueber-einen-quadratkilometer-verteilt/121546,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 14:50:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137326-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137326-177391-177388_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Eine neue Technologie verteilt 10 GBit/s über eine große Fläche. Der erste Feldversuch ist erfolgreich verlaufen. Zum ersten Mal wurde ein stabiles drahtloses Netzwerk bei diesen Frequenzen und mit diesen Datenraten betrieben. (<a href="https://www.golem.de/specials/wissenschaft/">Wissenschaft</a>, <a href="https://www.golem.de/specials/mobil/">Mobil</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137326&page=1&ts=1540475400" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>20</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Red Dead Redemption 2: Saloon-Prügelei (der Worte) im Livestream</title>
|
||||
<link>https://www.golem.de/news/red-dead-redemption-2-saloon-pruegelei-der-worte-im-livestream-1810-137312-rss.html</link>
|
||||
<description> Die Golem.de-Redakteure Peter Steinlechner und Michael Wieczorek diskutieren gemeinsam mit unserer Community über den Test zu Red Dead Redemption 2 live ab 18 Uhr. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137312&amp;page=1&amp;ts=1540474200" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/games/red-dead-redemption-2-saloon-pruegelei-der-worte-im-livestream/121545,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 14:30:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137312-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137312-177341-177338_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Die Golem.de-Redakteure Peter Steinlechner und Michael Wieczorek diskutieren gemeinsam mit unserer Community über den Test zu Red Dead Redemption 2 live ab 18 Uhr. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137312&page=1&ts=1540474200" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments />
|
||||
</item>
|
||||
<item>
|
||||
<title>Wolf Intelligence: Trojanerfirma aus Deutschland lässt interne Daten im Netz</title>
|
||||
<link>https://www.golem.de/news/wolf-intelligence-trojanerfirma-aus-deutschland-laesst-interne-daten-im-netz-1810-137323-rss.html</link>
|
||||
<description>Wolf Intelligence verkauft Schadsoftware an Staaten. Eine Sicherheitsfirma hat sensible Daten des Unternehmens öffentlich zugänglich im Internet gefunden. In einer Präsentation wurden die Funde gezeigt. (<a href="https://www.golem.de/specials/trojaner/">Trojaner</a>, <a href="https://www.golem.de/specials/virus/">Virus</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137323&amp;page=1&amp;ts=1540472400" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/wolf-intelligence-trojanerfirma-aus-deutschland-laesst-interne-daten-im-netz/121543,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 14:00:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137323-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137323-177383-177380_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Wolf Intelligence verkauft Schadsoftware an Staaten. Eine Sicherheitsfirma hat sensible Daten des Unternehmens öffentlich zugänglich im Internet gefunden. In einer Präsentation wurden die Funde gezeigt. (<a href="https://www.golem.de/specials/trojaner/">Trojaner</a>, <a href="https://www.golem.de/specials/virus/">Virus</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137323&page=1&ts=1540472400" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>16</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>NBN: Der Top-Nutzer verwendet 24 TByte im Monat</title>
|
||||
<link>https://www.golem.de/news/nbn-der-top-nutzer-verwendet-24-tbyte-im-monat-1810-137324-rss.html</link>
|
||||
<description> Ein staatliches FTTH-Netzwerk für fast alle in Australien bis 2017 war einst das Ziel. Doch davon ist beim (NBN) National Broadband Network nicht mehr viel übrig geblieben. In Berlin wurde eine Zwischenbilanz gezogen. (<a href="https://www.golem.de/specials/festnetz/">Festnetz</a>, <a href="https://www.golem.de/specials/dsl/">DSL</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137324&amp;page=1&amp;ts=1540471380" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/internet/nbn-der-top-nutzer-verwendet-24-tbyte-im-monat/121542,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 13:43:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137324-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137324-177387-177384_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Ein staatliches FTTH-Netzwerk für fast alle in Australien bis 2017 war einst das Ziel. Doch davon ist beim (NBN) National Broadband Network nicht mehr viel übrig geblieben. In Berlin wurde eine Zwischenbilanz gezogen. (<a href="https://www.golem.de/specials/festnetz/">Festnetz</a>, <a href="https://www.golem.de/specials/dsl/">DSL</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137324&page=1&ts=1540471380" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>51</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Linux-Kernel: Mit Machine Learning auf der Suche nach Bug-Fixes</title>
|
||||
<link>https://www.golem.de/news/linux-kernel-mit-machine-learning-auf-der-suche-nach-bug-fixes-1810-137321-rss.html</link>
|
||||
<description>Wichtige Patches, die in stabilen Kernel-Versionen landen sollten, werden von der Linux-Community oft vergessen oder übersehen. Abhilfe schaffen soll offenbar Machine Learning, wie die Entwickler Sasha Levin und Julia Lawall erklären. (<a href="https://www.golem.de/specials/linux-kernel/">Linux-Kernel</a>, <a href="https://www.golem.de/specials/linux/">Linux</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137321&amp;page=1&amp;ts=1540468800" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/security/linux-kernel-mit-machine-learning-auf-der-suche-nach-bug-fixes/121541,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 13:00:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137321-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137321-177375-177372_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Wichtige Patches, die in stabilen Kernel-Versionen landen sollten, werden von der Linux-Community oft vergessen oder übersehen. Abhilfe schaffen soll offenbar Machine Learning, wie die Entwickler Sasha Levin und Julia Lawall erklären. (<a href="https://www.golem.de/specials/linux-kernel/">Linux-Kernel</a>, <a href="https://www.golem.de/specials/linux/">Linux</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137321&page=1&ts=1540468800" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Quartalszahlen: AMDs Aktie gibt wegen mäßiger Aussichten nach</title>
|
||||
<link>https://www.golem.de/news/quartalszahlen-amds-aktie-gibt-wegen-maessiger-aussichten-nach-1810-137320-rss.html</link>
|
||||
<description>Im dritten Quartal 2018 konnte AMD zwar Umsatz und Gewinn steigern, aber nicht so stark wie erwartet. Die Aktie brach dennoch von über 25 US-Dollar auf 17 US-Dollar ein, da das vierte Quartal schlechter laufen wird als von den Anlegern gedacht - hier wurde zu viel erwartet. (<a href="https://www.golem.de/specials/amd/">AMD</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137320&amp;page=1&amp;ts=1540467600" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalszahlen-amds-aktie-gibt-wegen-maessiger-aussichten-nach/121540,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 12:40:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137320-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137320-177379-177376_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Im dritten Quartal 2018 konnte AMD zwar Umsatz und Gewinn steigern, aber nicht so stark wie erwartet. Die Aktie brach dennoch von über 25 US-Dollar auf 17 US-Dollar ein, da das vierte Quartal schlechter laufen wird als von den Anlegern gedacht - hier wurde zu viel erwartet. (<a href="https://www.golem.de/specials/amd/">AMD</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137320&page=1&ts=1540467600" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>17</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Projekt Broadband: Bahn will 3,5 Milliarden Euro vom Bund für Glasfasernetz</title>
|
||||
<link>https://www.golem.de/news/projekt-broadband-bahn-will-3-5-milliarden-euro-vom-bund-fuer-glasfasernetz-1810-137322-rss.html</link>
|
||||
<description>Die Pläne für das eigene Glasfasernetz der Deutschen Bahn werden konkret. Über die Finanzierung redet man jetzt mit der Regierung. Das Netz soll schnell gebaut werden. (<a href="https://www.golem.de/specials/deutsche-bahn/">Deutsche Bahn</a>, <a href="https://www.golem.de/specials/umts/">UMTS</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137322&amp;page=1&amp;ts=1540466580" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/internet/projekt-broadband-bahn-will-3-5-milliarden-euro-vom-bund-fuer-glasfasernetz/121539,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 12:23:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137322-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1808/136062-171340-171337_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Pläne für das eigene Glasfasernetz der Deutschen Bahn werden konkret. Über die Finanzierung redet man jetzt mit der Regierung. Das Netz soll schnell gebaut werden. (<a href="https://www.golem.de/specials/deutsche-bahn/">Deutsche Bahn</a>, <a href="https://www.golem.de/specials/umts/">UMTS</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137322&page=1&ts=1540466580" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>31</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Red Dead Redemption 2 im Test: Der Revolverhelden-Simulator</title>
|
||||
<link>https://www.golem.de/news/red-dead-redemption-2-im-test-der-revolverhelden-simulator-1810-137304-rss.html</link>
|
||||
<description>Reiten, prügeln, kochen, jagen, schießen, böse sein oder (relativ) brav: In Red Dead Redemption 2 gibt es enorme Möglichkeiten, sich als Revolverheld in einer wunderschönen Westernwelt auszuleben. Das Actionspiel von Rockstar Games ist ein großer Spaß - aber nicht ganz so gut wie GTA 5. Von Peter Steinlechner (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137304&amp;page=1&amp;ts=1540465260" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/games/red-dead-redemption-2-im-test-der-revolverhelden-simulator/121537,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 12:01:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137304-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137304-177303-177300_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Reiten, prügeln, kochen, jagen, schießen, böse sein oder (relativ) brav: In Red Dead Redemption 2 gibt es enorme Möglichkeiten, sich als Revolverheld in einer wunderschönen Westernwelt auszuleben. Das Actionspiel von Rockstar Games ist ein großer Spaß - aber nicht ganz so gut wie GTA 5. Von Peter Steinlechner (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137304&page=1&ts=1540465260" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>107</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Xiaomi: Das Mi Mix 3 hat keine Notch und eine versteckte Frontkamera</title>
|
||||
<link>https://www.golem.de/news/xiaomi-das-mi-mix-3-hat-keine-notch-und-eine-versteckte-frontkamera-1810-137319-rss.html</link>
|
||||
<description>Das Display von Xiaomis angekündigtem Smartphone Mi Mix 3 ist nahezu randlos. Die Frontkamera versteckt das Gerät hinter der aufschiebbaren Schale. Neu ist zudem, dass Xiaomi ein OLED-Panel verbaut und wieder den Qi-Ladestandard nutzt. (<a href="https://www.golem.de/specials/xiaomi/">Xiaomi</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137319&amp;page=1&amp;ts=1540464540" alt="" width="1" height="1" /></description>
|
||||
<comments>https://forum.golem.de/kommentare/handy/xiaomi-das-mi-mix-3-hat-keine-notch-und-eine-versteckte-frontkamera/121536,list.html</comments>
|
||||
<pubDate>Thu, 25 Oct 2018 11:49:00 +0100</pubDate>
|
||||
<guid>https://www.golem.de/1810/137319-rss.html</guid>
|
||||
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137319-177371-177370_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Das Display von Xiaomis angekündigtem Smartphone Mi Mix 3 ist nahezu randlos. Die Frontkamera versteckt das Gerät hinter der aufschiebbaren Schale. Neu ist zudem, dass Xiaomi ein OLED-Panel verbaut und wieder den Qi-Ladestandard nutzt. (<a href="https://www.golem.de/specials/xiaomi/">Xiaomi</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&aid=137319&page=1&ts=1540464540" alt="" width="1" height="1" />]]></content:encoded>
|
||||
<slash:comments>125</slash:comments>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user