├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build-api.yml │ └── build-gobot-covid.yml ├── .gitignore ├── .python-version ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.MD ├── app.json ├── db ├── 1.2 │ ├── places.sql │ ├── stories.sql │ └── users.sql ├── 1.3 │ └── status.sql └── 1.4 │ └── attachment.sql ├── manage.py └── src ├── __init__.py ├── app.py ├── bot.py ├── cache.py ├── config.py ├── cors.py ├── countries.json ├── crawler.py ├── db.py ├── gobot-covid ├── Dockerfile ├── go.mod ├── go.sum └── main.go ├── gunicorn_cfg.py ├── helper.py ├── limit.py ├── models.py ├── provinces.json ├── route ├── __init__.py ├── indonesia.py ├── maskmap.py └── root.py ├── seeder.py └── wsgi.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | dist 4 | .build 5 | docker/.data 6 | *.pyc 7 | Dockerfile 8 | .gitignore 9 | .gitlab-ci.yml 10 | k8s 11 | 12 | /src/gobot-covid/session/* 13 | /src/gobot-covid/session 14 | /src/gobot-covid/BUILD 15 | /src/gobot-covid/BUILD/* 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{py,tsx,shpaml}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | # Matches the exact files either package.json or .travis.yml 18 | [{*.json,.gitlab-ci.yml}] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/workflows/build-api.yml: -------------------------------------------------------------------------------- 1 | name: Build the api server 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | python-version: [3.7, 3.8] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Login to DockerHub Registry 20 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 21 | - name: Get the version 22 | id: vars 23 | run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10} 24 | - name: Build the tagged Docker image 25 | run: docker build --tag docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:${{steps.vars.outputs.tag}} . 26 | - name: Push the tagged Docker image 27 | run: docker push docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:${{steps.vars.outputs.tag}} 28 | - name: Build the latest Docker image 29 | run: docker build --tag docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:latest . 30 | - name: Push the latest Docker image 31 | run: docker push docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:latest 32 | -------------------------------------------------------------------------------- /.github/workflows/build-gobot-covid.yml: -------------------------------------------------------------------------------- 1 | name: Build Golang Bot Covid-19 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | 9 | build: 10 | 11 | strategy: 12 | matrix: 13 | go-version: [1.14.x] 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Login to DockerHub Registry 19 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 20 | - name: Get the version 21 | id: vars 22 | run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10} 23 | - name: Build the tagged Docker image 24 | run: docker build --tag docker.pkg.github.com/k1m0ch1/covid-19-api/gobot-covid:${{steps.vars.outputs.tag}} -f src/gobot-covid/Dockerfile src/gobot-covid/. 25 | - name: Push the tagged Docker image 26 | run: docker push docker.pkg.github.com/k1m0ch1/covid-19-api/gobot-covid:${{steps.vars.outputs.tag}} 27 | - name: Build the latest Docker image 28 | run: docker build --tag docker.pkg.github.com/k1m0ch1/covid-19-api/gobot-covid:latest -f src/gobot-covid/Dockerfile src/gobot-covid/. 29 | - name: Push the latest Docker image 30 | run: docker push docker.pkg.github.com/k1m0ch1/covid-19-api/gobot-covid:latest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /.build 4 | /docker/.data 5 | *.pyc 6 | 7 | /.vscode 8 | /src/gobot-covid/session/* 9 | /src/gobot-covid/session 10 | 11 | /src/gobot-covid/BUILD 12 | /src/gobot-covid/BUILD/* 13 | 14 | chromedriver 15 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.3 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | RUN apk add \ 3 | --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing \ 4 | --no-cache \ 5 | build-base libffi-dev python3-dev py3-lxml \ 6 | libxml2 libxml2-dev libxslt-dev postgresql-dev openssh git \ 7 | libxml2-dev libxslt-dev chromium-chromedriver chromium file \ 8 | imagemagick bash pngcrush optipng=0.7.7-r0 imagemagick-dev 9 | 10 | RUN pip install --upgrade pip && pip install pipenv gunicorn 11 | 12 | WORKDIR /app 13 | 14 | COPY Pipfile Pipfile.lock ./ 15 | 16 | RUN pipenv install --system --deploy 17 | 18 | COPY src/ ./src/ 19 | COPY manage.py ./manage.py 20 | 21 | ENV PATH="/usr/bin/chromedriver:${PATH}" 22 | ENV CHROMEDRIVER="/usr/bin/chromedriver" 23 | 24 | ENTRYPOINT ["python", "manage.py"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 k1m0ch1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | alembic = "*" 10 | fire = "*" 11 | cachetools = "*" 12 | requests = "*" 13 | flask = "*" 14 | flask-caching = "*" 15 | gunicorn = "*" 16 | flask-login = "*" 17 | sqlalchemy-utils = "*" 18 | shortuuid = "*" 19 | psycopg2-binary = "*" 20 | sqlalchemy = "*" 21 | flask-limiter = "*" 22 | beautifulsoup4 = "*" 23 | flask-cors = "*" 24 | selenium = "*" 25 | wand = "*" 26 | 27 | [requires] 28 | python_version = "3.7" 29 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "68e03aba794443c943296d87eb407fc135b3e82133dee511f74a808778c9c9b2" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf" 22 | ], 23 | "index": "pypi", 24 | "version": "==1.4.2" 25 | }, 26 | "beautifulsoup4": { 27 | "hashes": [ 28 | "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a", 29 | "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887", 30 | "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae" 31 | ], 32 | "index": "pypi", 33 | "version": "==4.8.2" 34 | }, 35 | "cachetools": { 36 | "hashes": [ 37 | "sha256:9a52dd97a85f257f4e4127f15818e71a0c7899f121b34591fcc1173ea79a0198", 38 | "sha256:b304586d357c43221856be51d73387f93e2a961598a9b6b6670664746f3b6c6c" 39 | ], 40 | "index": "pypi", 41 | "version": "==4.0.0" 42 | }, 43 | "certifi": { 44 | "hashes": [ 45 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", 46 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" 47 | ], 48 | "version": "==2019.11.28" 49 | }, 50 | "chardet": { 51 | "hashes": [ 52 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 53 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 54 | ], 55 | "version": "==3.0.4" 56 | }, 57 | "click": { 58 | "hashes": [ 59 | "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", 60 | "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" 61 | ], 62 | "version": "==7.1.1" 63 | }, 64 | "fire": { 65 | "hashes": [ 66 | "sha256:96c372096afcf33ddbadac8a7ca5b7e829e8d7157d0030bd964bf959afde5c2c" 67 | ], 68 | "index": "pypi", 69 | "version": "==0.3.0" 70 | }, 71 | "flask": { 72 | "hashes": [ 73 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 74 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 75 | ], 76 | "index": "pypi", 77 | "version": "==1.1.1" 78 | }, 79 | "flask-caching": { 80 | "hashes": [ 81 | "sha256:3d0bd13c448c1640334131ed4163a12aff7df2155e73860f07fc9e5e75de7126", 82 | "sha256:54b6140bb7b9f3e63d009ff08b03bacd84eefb1af1d30af06b4a6bc3c16fa3b2" 83 | ], 84 | "index": "pypi", 85 | "version": "==1.8.0" 86 | }, 87 | "flask-cors": { 88 | "hashes": [ 89 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 90 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 91 | ], 92 | "index": "pypi", 93 | "version": "==3.0.8" 94 | }, 95 | "flask-limiter": { 96 | "hashes": [ 97 | "sha256:d984a57ef37acb6eee29edc864ff22cd4cf090845f06968c015093ffd91e96f1", 98 | "sha256:db2a069402977927282b0fcf650753bfcb50488028def9f5b2398e1d525f2f9f" 99 | ], 100 | "index": "pypi", 101 | "version": "==1.2.1" 102 | }, 103 | "flask-login": { 104 | "hashes": [ 105 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 106 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 107 | ], 108 | "index": "pypi", 109 | "version": "==0.5.0" 110 | }, 111 | "gunicorn": { 112 | "hashes": [ 113 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 114 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 115 | ], 116 | "index": "pypi", 117 | "version": "==20.0.4" 118 | }, 119 | "idna": { 120 | "hashes": [ 121 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 122 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 123 | ], 124 | "version": "==2.9" 125 | }, 126 | "itsdangerous": { 127 | "hashes": [ 128 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 129 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 130 | ], 131 | "version": "==1.1.0" 132 | }, 133 | "jinja2": { 134 | "hashes": [ 135 | "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", 136 | "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" 137 | ], 138 | "version": "==2.11.1" 139 | }, 140 | "limits": { 141 | "hashes": [ 142 | "sha256:0e5f8b10f18dd809eb2342f5046eb9aa5e4e69a0258567b5f4aa270647d438b3", 143 | "sha256:f0c3319f032c4bfad68438ed1325c0fac86dac64582c7c25cddc87a0b658fa20" 144 | ], 145 | "version": "==1.5.1" 146 | }, 147 | "mako": { 148 | "hashes": [ 149 | "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d", 150 | "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9" 151 | ], 152 | "version": "==1.1.2" 153 | }, 154 | "markupsafe": { 155 | "hashes": [ 156 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 157 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 158 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 159 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 160 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 161 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 162 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 163 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 164 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 165 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 166 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 167 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 168 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 169 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 170 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 171 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 172 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 173 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 174 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 175 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 176 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 177 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 178 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 179 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 180 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 181 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 182 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 183 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 184 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 185 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 186 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 187 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 188 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 189 | ], 190 | "version": "==1.1.1" 191 | }, 192 | "psycopg2-binary": { 193 | "hashes": [ 194 | "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", 195 | "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", 196 | "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", 197 | "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", 198 | "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", 199 | "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", 200 | "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", 201 | "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", 202 | "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", 203 | "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", 204 | "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", 205 | "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", 206 | "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", 207 | "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", 208 | "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", 209 | "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", 210 | "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", 211 | "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", 212 | "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", 213 | "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", 214 | "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", 215 | "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", 216 | "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1", 217 | "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", 218 | "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", 219 | "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", 220 | "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", 221 | "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", 222 | "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f", 223 | "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", 224 | "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", 225 | "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" 226 | ], 227 | "index": "pypi", 228 | "version": "==2.8.4" 229 | }, 230 | "python-dateutil": { 231 | "hashes": [ 232 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 233 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 234 | ], 235 | "version": "==2.8.1" 236 | }, 237 | "python-editor": { 238 | "hashes": [ 239 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 240 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 241 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" 242 | ], 243 | "version": "==1.0.4" 244 | }, 245 | "requests": { 246 | "hashes": [ 247 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 248 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 249 | ], 250 | "index": "pypi", 251 | "version": "==2.23.0" 252 | }, 253 | "selenium": { 254 | "hashes": [ 255 | "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", 256 | "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d" 257 | ], 258 | "index": "pypi", 259 | "version": "==3.141.0" 260 | }, 261 | "shortuuid": { 262 | "hashes": [ 263 | "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f", 264 | "sha256:492c7402ff91beb1342a5898bd61ea953985bf24a41cd9f247409aa2e03c8f77" 265 | ], 266 | "index": "pypi", 267 | "version": "==1.0.1" 268 | }, 269 | "six": { 270 | "hashes": [ 271 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 272 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 273 | ], 274 | "version": "==1.14.0" 275 | }, 276 | "soupsieve": { 277 | "hashes": [ 278 | "sha256:e914534802d7ffd233242b785229d5ba0766a7f487385e3f714446a07bf540ae", 279 | "sha256:fcd71e08c0aee99aca1b73f45478549ee7e7fc006d51b37bec9e9def7dc22b69" 280 | ], 281 | "version": "==2.0" 282 | }, 283 | "sqlalchemy": { 284 | "hashes": [ 285 | "sha256:c4cca4aed606297afbe90d4306b49ad3a4cd36feb3f87e4bfd655c57fd9ef445" 286 | ], 287 | "index": "pypi", 288 | "version": "==1.3.15" 289 | }, 290 | "sqlalchemy-utils": { 291 | "hashes": [ 292 | "sha256:f268af5bc03597fe7690d60df3e5f1193254a83e07e4686f720f61587ec4493a" 293 | ], 294 | "index": "pypi", 295 | "version": "==0.36.3" 296 | }, 297 | "termcolor": { 298 | "hashes": [ 299 | "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" 300 | ], 301 | "version": "==1.1.0" 302 | }, 303 | "urllib3": { 304 | "hashes": [ 305 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", 306 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" 307 | ], 308 | "version": "==1.25.8" 309 | }, 310 | "wand": { 311 | "hashes": [ 312 | "sha256:598e13e46779e48fcecba7b37fd9d61fcdd1e70007ccba5d5b2e731186a2ec2e", 313 | "sha256:6eaca78e53fbe329b163f0f0b28f104de98edbd69a847268cc5d6a6e392b9b28" 314 | ], 315 | "index": "pypi", 316 | "version": "==0.5.9" 317 | }, 318 | "werkzeug": { 319 | "hashes": [ 320 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 321 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 322 | ], 323 | "version": "==1.0.1" 324 | } 325 | }, 326 | "develop": {} 327 | } 328 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn --config python:src.gunicorn_cfg src.wsgi:app 2 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Coronavirus Disease 19 API Tracker 2 | 3 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/k1m0ch1/covid-19-api) 4 | ![GitHub repo size](https://img.shields.io/github/repo-size/k1m0ch1/covid-19-api) 5 | ![GitHub last commit](https://img.shields.io/github/last-commit/k1m0ch1/covid-19-api) 6 | ![GitHub stars](https://img.shields.io/github/stars/k1m0ch1/covid-19-api) 7 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/k1m0ch1/covid-19-api) 8 | ![GitHub forks](https://img.shields.io/github/forks/k1m0ch1/covid-19-api) 9 | ![GitHub issues](https://img.shields.io/github/issues/k1m0ch1/covid-19-api) 10 | ![GitHub watchers](https://img.shields.io/github/watchers/k1m0ch1/covid-19-api) 11 | ![Twitter](https://img.shields.io/twitter/follow/bukanyahya?style=social) 12 | 13 | This is just a simple API tracker for covid-19 latest information for recovered, confirmed and deaths cases. The main purpose of this repo is for bot announcement to whatsapp, telegram and discord about latest cases of covid-19. 14 | 15 | you can try the api here [https://covid19-api.yggdrasil.id/](https://covid19-api.yggdrasil.id/) ![Website](https://img.shields.io/website?down_message=offline&up_message=online&url=https://covid19-api.yggdrasil.id/) thanks for @habibiefaried for helping the server 16 | or [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 17 | 18 | # Bot Whatsapp 19 | (WARNING: This is only available with Bahasa Language, need some help for translation) 20 | This bot is for covid-19 information purpose by automatically answer for available data, by using `!covid `, here is the available command : 21 | 1. status 22 | This will return information about current global case, example output 23 | ``` 24 | Confirmed: xx 25 | Deaths: xx 26 | Recovered: xx 27 | ``` 28 | 2. id 29 | This will return information about current indonesia corona case, example output 30 | ``` 31 | Terkonfirmasi: xx 32 | Meninggal: xx 33 | Sembuh: xx 34 | Dalam Perawatan: xx 35 | 36 | Update Terakhir: 37 | 2020-03-17T07:01:01+00:00 38 | ``` 39 | 3. id info 40 | This will return basic information about corona in indonesia 41 | 4. ping 42 | The bot will response "pong" 43 | 5. halo, hello, help 44 | The introduction and list of available command 45 | ## Requirement 46 | Docker 47 | ## Installation 48 | run this command 49 | ``` 50 | docker run --restart on-failure --name gobot-covid k1m0ch1/gobot-covid 51 | ``` 52 | The QR Code will be available in terminal for more less than 10 second, if timeout you need to run the first command again 53 | after the QR Code is succesfully scanned I prefer to run the container in the background by stop the container first with `Ctrl+C` and then run the container again with command `docker start gobot-covid` 54 | 55 | ### For advance usage 56 | - Session store 57 | This feature will enable to store the session for the later use, by adding the argument as the file name for sesion for example 58 | ``` 59 | docker run --restart on-failure --name gobot-covid k1m0ch1/gobot-covid MyPhoneNumber 60 | ``` 61 | the file `MyPhoneNumber.go` will be stored at `./session` folder 62 | 63 | the folder `session` in the container at path `/go/src/github.com/k1m0ch1/covid-19-api/session` so you can use `-v` parameter to put the session file in the localhost 64 | 65 | here is the complete parameter 66 | ``` 67 | docker run -v ~/session:/go/src/github.com/k1m0ch1/covid-19-api/session x--restart on-failure --name gobot-covid k1m0ch1/gobot-covid MyPhoneNumber 68 | ``` 69 | 70 | # Endpoint 71 | 72 | ## Get latest information about confirmed, deaths and recovered 73 | ``` 74 | GET / 75 | ``` 76 | 77 | ``` 78 | {"Confirmed":167447,"Deaths":6440,"Recovered":76034} 79 | ``` 80 | ## Get each countries confirmed, deaths, and recovered cases 81 | ``` 82 | GET /confirmed 83 | ``` 84 | 85 | ``` 86 | [ 87 | ..., 88 | { 89 | "confirmed": 14991, 90 | "countryRegion": "Iran", 91 | "lastUpdate": "2020-03-16T14:38:45", 92 | "latitude": "32.4279", 93 | "longitude": "53.6880", 94 | "provinceState": null 95 | } 96 | ..., 97 | ] 98 | ``` 99 | same as result with endpoint `GET /deaths` and `GET /recovered` 100 | ## Get latest information about all country cases status 101 | ``` 102 | GET /all 103 | ``` 104 | ``` 105 | [ 106 | ..., 107 | { 108 | "Confirmed": xx, 109 | "CountryRegion": "CountryName", 110 | "Deaths": xx, 111 | "LastUpdate": , 112 | "Latitude": "xx.x", 113 | "Longitude": "-xx.x", 114 | "ProvinceState": "State" or null, 115 | "Recovered": xx 116 | }, 117 | ... 118 | ] 119 | ``` 120 | ## List of available countries 121 | ``` 122 | GET /countries 123 | ``` 124 | ## List of available countries detail cases 125 | ``` 126 | GET /countries/ 127 | ``` 128 | ``` 129 | GET /countries/us 130 | ``` 131 | ``` 132 | [ 133 | ..., 134 | { 135 | "confirmed": 557, 136 | "countryRegion": "US", 137 | "deaths": 7, 138 | "lastUpdate": "2020-03-16T23:53:03", 139 | "latitude": "36.1162", 140 | "longitude": "-119.6816", 141 | "provinceState": "California", 142 | "recovered": 6 143 | }, 144 | ..., 145 | { 146 | "summary": { 147 | "confirmed": 4632, 148 | "deaths": 85, 149 | "recovered": 17 150 | } 151 | } 152 | ..., 153 | ] 154 | ``` 155 | if the country only have a single data, the summary data will not exist 156 | 157 | ## List of available news 158 | ``` 159 | GET /news 160 | ``` 161 | get the latest top headlines about corona virus, 162 | the news is get from `NewsAPI.org` see there for available news 163 | 164 | ## List of available news in specific countries 165 | ``` 166 | GET /news/ 167 | ``` 168 | get the latest top headlines about corona virus in specific countries, 169 | the news is get from `NewsAPI.org` see there for available news 170 | 171 | # Docker 172 | You can use docker by pull 173 | ``` 174 | docker run --name covid-19-api -p 5001:5001 -d docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:latest run 175 | ``` 176 | or 177 | ``` 178 | docker run --name covid-19-api -p 5000:5000 -d docker.pkg.github.com/k1m0ch1/covid-19-api/covid-19-api:latest run_web_prod 179 | ``` 180 | 181 | # Development 182 | This repo is still on development and feel free to contribute. 183 | ## Requirement 184 | 1. Python 3.7 185 | 2. pipenv/ pip 186 | 3. venv/ virtualenv 187 | 188 | ## Running on development 189 | ``` 190 | pipenv run python manage.py run 191 | ``` 192 | 193 | ## Running on production 194 | ``` 195 | pipenv run python manage.py run_web_prod 196 | or 197 | gunicorn --config python:src.gunicorn_cfg src.wsgi:app 198 | ``` 199 | 200 | ## Next to do : 201 | - ~~deploy to server for public use~~ deployed to [https://covid19-api.yggdrasil.id/](https://covid19-api.yggdrasil.id/) Thanks @habibiefaried 202 | - ~~add new endpoint for each countries~~ referrence from @mathdroid 203 | - ~~add new endoint `/confirmed` `/deaths` and `/recovered`~~ 204 | - bot discord 205 | - ~~bot whatsapp~~ 206 | - bot telegram 207 | - news crawler from reliable resource 208 | 209 | ## Bot Whatsapp to do : 210 | - ~~session is stored into specific volume, so can be used later~~ 211 | - ~~prevent the whatsapp spam~~ 212 | - make much more simple way to run this bot for cross platform 213 | - ~~for command `id` need to add development of the number before and after~~ 214 | - for command `id` add a linear graph better get from https://kawalcovid19.blob.core.windows.net/viz/statistik_harian.html 215 | - ~~add a new command `jabar` scrape from https://pikobar.jabarprov.go.id/~~ 216 | - add a new command `jkt` scrape from http://corona.jakarta.go.id/ 217 | - add `id info` for much more detail information 218 | - add `id hotline` for much more detail information 219 | 220 | # Referrence 221 | - This repo is inspired from @mathdroid repo https://github.com/mathdroid/covid-19-api, some of the similiar is credited to @mathdroid 222 | - Global case data is parsed from [2019 Novel Coronavirus COVID-19 (2019-nCoV) Data Repository by Johns Hopkins CSSE](https://github.com/CSSEGISandData/COVID-19) 223 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Coronavirus Disease 19 API Tracker", 3 | "description": "🍰 Simple and 🦅 fast covid-19 api global data parser using Flask, data gathered from Novel Coronavirus (COVID-19) Cases, provided by JHU CSSE", 4 | "repository": "https://github.com/k1m0ch1/covid-19-api", 5 | "keywords": ["covid19", "flask", "python"] 6 | } 7 | -------------------------------------------------------------------------------- /db/1.2/places.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for places 3 | -- ---------------------------- 4 | DROP TABLE IF EXISTS "public"."places"; 5 | CREATE TABLE "public"."places" ( 6 | "id" varchar(22) COLLATE "pg_catalog"."default" NOT NULL, 7 | "name" text COLLATE "pg_catalog"."default" NOT NULL, 8 | "lng" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, 9 | "lat" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, 10 | "description" text COLLATE "pg_catalog"."default", 11 | "created" timestamp(6) NOT NULL DEFAULT timezone('utc'::text, now()), 12 | "updated" timestamp(6) 13 | ) 14 | ; 15 | COMMENT ON COLUMN "public"."places"."id" IS 'uuid'; 16 | 17 | -- ---------------------------- 18 | -- Primary Key structure for table places 19 | -- ---------------------------- 20 | ALTER TABLE "public"."places" ADD CONSTRAINT "places_pkey" PRIMARY KEY ("id"); 21 | -------------------------------------------------------------------------------- /db/1.2/stories.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for stories 3 | -- ---------------------------- 4 | DROP TABLE IF EXISTS "public"."stories"; 5 | CREATE TABLE "public"."stories" ( 6 | "id" varchar(22) COLLATE "pg_catalog"."default" NOT NULL, 7 | "place_id" varchar(22) COLLATE "pg_catalog"."default", 8 | "user_id" varchar(22) COLLATE "pg_catalog"."default", 9 | "availability" varchar(100) COLLATE "pg_catalog"."default" NOT NULL, 10 | "num" int8, 11 | "price" int8, 12 | "validity" varchar(50) COLLATE "pg_catalog"."default", 13 | "created" timestamp(6) DEFAULT timezone('utc'::text, now()), 14 | "updated" timestamp(6) 15 | ) 16 | ; 17 | 18 | -- ---------------------------- 19 | -- Primary Key structure for table stories 20 | -- ---------------------------- 21 | ALTER TABLE "public"."stories" ADD CONSTRAINT "stories_pkey" PRIMARY KEY ("id"); 22 | -------------------------------------------------------------------------------- /db/1.2/users.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for users 3 | -- ---------------------------- 4 | DROP TABLE IF EXISTS "public"."users"; 5 | CREATE TABLE "public"."users" ( 6 | "id" varchar(22) COLLATE "pg_catalog"."default" NOT NULL, 7 | "name" varchar(100) COLLATE "pg_catalog"."default", 8 | "email" varchar(100) COLLATE "pg_catalog"."default", 9 | "birthdate" date, 10 | "phone" varchar(16) COLLATE "pg_catalog"."default", 11 | "created" timestamp(6) NOT NULL DEFAULT timezone('utc'::text, now()), 12 | "updated" timestamp(6) 13 | ) 14 | ; 15 | COMMENT ON COLUMN "public"."users"."id" IS 'short uuid'; 16 | 17 | -- ---------------------------- 18 | -- Primary Key structure for table users 19 | -- ---------------------------- 20 | ALTER TABLE "public"."users" ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id"); 21 | -------------------------------------------------------------------------------- /db/1.3/status.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for status 3 | -- ---------------------------- 4 | DROP TABLE IF EXISTS "public"."status"; 5 | CREATE SEQUENCE status_id_seq; 6 | 7 | CREATE TABLE status ( 8 | id integer NOT NULL DEFAULT nextval('status_id_seq'), 9 | confirmed integer NOT NULL, 10 | recovered integer NOT NULL, 11 | active_care integer NOT NULL, 12 | deaths integer NOT NULL, 13 | country_id VARCHAR NOT NULL, 14 | created TIMESTAMP, 15 | updated TIMESTAMP 16 | ); 17 | 18 | ALTER SEQUENCE status_id_seq OWNED BY status.id; 19 | -------------------------------------------------------------------------------- /db/1.4/attachment.sql: -------------------------------------------------------------------------------- 1 | -- ---------------------------- 2 | -- Table structure for attachment 3 | -- ---------------------------- 4 | CREATE TABLE "public"."attachment" ( 5 | "id" varchar(22) COLLATE "pg_catalog"."default" NOT NULL, 6 | "key" varchar(70) COLLATE "pg_catalog"."default", 7 | "name" varchar(50) COLLATE "pg_catalog"."default", 8 | "description" text COLLATE "pg_catalog"."default", 9 | "attachment" text COLLATE "pg_catalog"."default" NOT NULL, 10 | "created" timestamp(0) NOT NULL DEFAULT timezone('utc'::text, now()), 11 | "updated" timestamp(0) 12 | ) 13 | ; 14 | 15 | -- ---------------------------- 16 | -- Primary Key structure for table attachment 17 | -- ---------------------------- 18 | ALTER TABLE "public"."attachment" ADD CONSTRAINT "attachment_pkey" PRIMARY KEY ("id"); 19 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import fire 2 | import os 3 | 4 | from src.wsgi import app 5 | from src import seeder 6 | 7 | 8 | def run_scrapper(): 9 | print("run scrapper") 10 | 11 | 12 | def clear_cache(): 13 | print("clear cache") 14 | 15 | 16 | def run_web(): 17 | app.run(host='0.0.0.0', port=5001, debug=True) 18 | 19 | 20 | def run_web_prod(): 21 | """Run web application in production""" 22 | 23 | _execvp([ 24 | "gunicorn", "--config", "python:src.gunicorn_cfg", "src.wsgi:app" 25 | ]) 26 | 27 | 28 | def run_seed(): 29 | seeder.seed() 30 | 31 | 32 | def test(): 33 | print("unittest") 34 | 35 | 36 | def _execvp(args): 37 | os.execvp(args[0], args) 38 | 39 | 40 | if __name__ == '__main__': 41 | fire.Fire() 42 | 43 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k1m0ch1/covid-19-api/40a4bc772ea18de542a16c847e9a91c0e2a534af/src/__init__.py -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .route import register_route 3 | from src import cache, db, limit, cors 4 | 5 | 6 | def create_app(): 7 | app = Flask(__name__) 8 | 9 | register_route(app) 10 | cache.init_app(app) 11 | db.init_app(app) 12 | limit.init_app(app) 13 | cors.init_app(app) 14 | 15 | return app 16 | -------------------------------------------------------------------------------- /src/bot.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import dateutil 3 | 4 | from src.helper import gen 5 | 6 | # flake8: noqa 7 | 8 | def summary(data): 9 | return f"*Global Cases*\n\nConfirmed: {gen(data['confirmed'])} \n" \ 10 | f"Deaths: {gen(data['deaths'])} \nRecovered: {gen(data['recovered'])}" 11 | 12 | 13 | def countries(data): 14 | waktu = dateutil.parser.parse(data['last_updated']) + timedelta(hours=7) 15 | waktu = datetime.strftime(waktu, "%d %b %Y %H:%M") 16 | 17 | footer = f"Last Updated: {waktu}\nData Source: https://git.io/Jvoxz" 18 | confirmed, deaths, recovered = 0, 0, 0 19 | c_diff, d_diff, r_diff = 0, 0, 0 20 | header = f"*{data['diff'][0]['countryRegion']}*" 21 | for item in data['origin']: 22 | confirmed += int(item['confirmed']) 23 | deaths += int(item['deaths']) 24 | recovered += int(item['recovered']) 25 | for item in data['diff']: 26 | c_diff += int(item['confirmed']) 27 | d_diff += int(item['deaths']) 28 | r_diff += int(item['recovered']) 29 | Confirmed = f"Confirmed: {gen(confirmed)} *(+{gen(confirmed - c_diff)})*" 30 | Deaths = f"Deaths: {gen(deaths)} *(+{gen(deaths - d_diff)})*" 31 | Recovered = f"Recovered: {gen(recovered)} *(+{gen(recovered - r_diff)})*" 32 | return f"{header}\n\n{Confirmed}\n{Deaths}\n{Recovered}\n\n{footer}" 33 | 34 | 35 | def id(data): 36 | waktu = dateutil.parser.parse(data['metadata']['last_updated']) + timedelta(hours=7) 37 | waktu = datetime.strftime(waktu, "%d %b %Y %H:%M") 38 | 39 | return "*Indonesia*\n\n" \ 40 | f"Terkonfirmasi: {gen(data['confirmed']['value'])} *(+{gen(data['confirmed']['diff'])})*\n" \ 41 | f"Meninggal: {gen(data['deaths']['value'])} *(+{gen(data['deaths']['diff'])})*\n" \ 42 | f"Sembuh: {gen(data['recovered']['value'])} *(+{gen(data['recovered']['diff'])})*\n" \ 43 | f"Dalam Perawatan: {gen(data['active_care']['value'])} *(+{gen(data['active_care']['diff'])})*\n\n" \ 44 | f"Update Terakhir {waktu}\nSumber data https://kawalcovid19.id" 45 | 46 | 47 | def province(data): 48 | waktu = dateutil.parser.parse(data['metadata']['source_date']) + timedelta(hours=7) 49 | waktu = datetime.strftime(waktu, "%d %b %Y %H:%M") 50 | 51 | result = f"*{data['metadata']['province']}*" 52 | 53 | sembuh_diff = f"*(+{gen(data['total_sembuh']['diff'])})*" if data['total_sembuh']['diff'] > 0 else "" 54 | sembuh = f"Sembuh: {gen(data['total_sembuh']['value'])} {sembuh_diff}" 55 | 56 | positif_diff = f"*(+{gen(data['total_positif']['diff'])})*" if data['total_positif']['diff'] > 0 else "" 57 | positif = f"Positif: {gen(data['total_positif']['value'])} {positif_diff}" 58 | 59 | meninggal_diff = f"*(+{gen(data['total_meninggal']['diff'])})*" if data['total_meninggal']['diff'] > 0 else "" 60 | meninggal = f"Meninggal: {gen(data['total_meninggal']['value'])} {meninggal_diff}" 61 | 62 | footer = f"Update Terakhir {waktu}\nSumber data : {data['metadata']['source']}" 63 | 64 | return f"{result}\n\n{sembuh}\n{positif}\n{meninggal}\n\n{footer}" 65 | 66 | 67 | def jabar(data): 68 | waktu = dateutil.parser.parse(data['metadata']['source_date']) + timedelta(hours=7) 69 | waktu = datetime.strftime(waktu, "%d %b %Y %H:%M") 70 | 71 | result = f"*{data['metadata']['province']}*" 72 | 73 | s_diff = int(data['total_sembuh']['value']-data['total_sembuh']['diff']) 74 | sembuh_diff = f"*(+{gen(s_diff)})*" if s_diff > 0 else "" 75 | sembuh = f"Sembuh: {gen(data['total_sembuh']['value'])} {sembuh_diff}" 76 | 77 | p_diff = data['total_positif_saat_ini']['value']-data['total_positif_saat_ini']['diff'] 78 | positif_diff = f"*(+{gen(p_diff)})*" if p_diff > 0 else "" 79 | positif = f"Positif: {gen(data['total_positif_saat_ini']['value'])} {positif_diff}" 80 | 81 | m_diff = data['total_meninggal']['value'] - data['total_meninggal']['diff'] 82 | meninggal_diff = f"*(+{gen(m_diff)})*" if m_diff > 0 else "" 83 | meninggal = f"Meninggal: {gen(data['total_meninggal']['value'])} {meninggal_diff}" 84 | 85 | proses_pemantauan = f'Proses Pemantauan: {gen(data["proses_pemantauan"]["value"])}' 86 | selesai_pemantauan = f'Selesai Pemantauan: {gen(data["selesai_pemantauan"]["value"])}' 87 | 88 | proses_pengawasan = f'Proses Pengawasan: {gen(data["proses_pengawasan"]["value"])}' 89 | selesai_pengawasan = f'Selesai Pengawasan: {gen(data["selesai_pengawasan"]["value"])}' 90 | 91 | odp = f'ODP: {gen(data["total_odp"]["value"])}' 92 | pdp = f'PDP: {gen(data["total_pdp"]["value"])}' 93 | 94 | footer = f"Update Terakhir {waktu}\nSumber data : {data['metadata']['source']}" 95 | 96 | return f"{result}\n\n{sembuh}\n{positif}\n{meninggal}\n{proses_pemantauan}\n{selesai_pemantauan}\n" \ 97 | f"{proses_pengawasan}\n{selesai_pengawasan}\n{odp}\n{pdp}\n\n{footer}" 98 | 99 | 100 | def province_list(data): 101 | header = "*List Provinsi yang tersedia*" 102 | footer = "Contoh Penggunaan : *!covid id papua*" 103 | result = "" 104 | for index, value in enumerate(data): 105 | if index%8 == 0 : 106 | result +="\n\n" 107 | result += f"{value}, " 108 | return f"{header}{result}\n\n{footer}" 109 | 110 | 111 | def news(data): 112 | result = "*Top Headline News*\n\n" 113 | for item in data: 114 | result += f"{item['title']}\n{item['url']}\n\n" 115 | return result + "Cermat dalam mengamati berita dan hindari hoaks\nBantu kami di https://git.io/JvPbJ ❤️" 116 | 117 | 118 | def hotline(data): 119 | header = "*List Hotline*\n\n" 120 | result = "" 121 | for index, item in enumerate(data): 122 | result += f"*{item['kota']}*\n" 123 | result += "Call Center: " 124 | for row in item['callCenter']: 125 | result += f"{row}, " 126 | result += f"\nHotline: " 127 | for row in item['hotline']: 128 | result += f"{row}, " 129 | result += "\n\n" 130 | return result 131 | 132 | 133 | def introduction(): 134 | return "*Halo* 🤗\n\nPerkenalan saya robot covid-19 untuk mendapatkan informasi tentang covid," \ 135 | "panggil saya menggunakan awalan !covid\n\nPerintah yang tersedia :\n1. status (global cases)\n" \ 136 | "2. hotline\n3. news \n4. id (indonesia cases)" \ 137 | "\n5. id " \ 138 | "\n6. id info\n7. id news (top headline news indonesia)" \ 139 | "\n8. halo\n\nContoh : !covid id jabar\n" \ 140 | "\n\nBantu kami di https://git.io/JvPbJ ❤️" 141 | -------------------------------------------------------------------------------- /src/cache.py: -------------------------------------------------------------------------------- 1 | from flask_caching import Cache 2 | 3 | 4 | cache = Cache(config={'CACHE_TYPE': 'simple'}) 5 | 6 | 7 | def init_app(app): 8 | cache.init_app(app) 9 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | _ = environ.get 4 | 5 | 6 | DBSTRING = _('DBSTRING', 7 | 'postgresql://untitled-web:untitled-web@localhost/covid19api') 8 | WEBDRIVER = _('WEBDRIVER', './chromedriver') 9 | 10 | DATASET_ALL = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/%s.csv" # noqa 11 | NEWSAPI_KEY = _('NEWSAPI_KEY', "xxxxxxxxxx") 12 | 13 | NEWSAPI_HOST = _('NEWSAPI_HOST', "http://newsapi.org/v2/top-headlines") 14 | sLIMITER = 5 15 | 16 | CHROMEDRIVER = _('CHROMEDRIVER', "./chromedriver") 17 | STATISTIK_URL = 'https://kawalcovid19.blob.core.windows.net/viz/statistik_harian.html' # noqa 18 | -------------------------------------------------------------------------------- /src/cors.py: -------------------------------------------------------------------------------- 1 | 2 | from flask_cors import CORS 3 | 4 | cors = CORS(resources={r"/maskmap/*": {"origins": "*"}}) 5 | 6 | 7 | def init_app(app): 8 | cors.init_app(app) 9 | -------------------------------------------------------------------------------- /src/countries.json: -------------------------------------------------------------------------------- 1 | {"countries":{"Afghanistan":"AF","Aland Islands":"AX","Albania":"AL","Algeria":"DZ","American Samoa":"AS","Andorra":"AD","Angola":"AO","Anguilla":"AI","Antarctica":"AQ","Antigua and Barbuda":"AG","Argentina":"AR","Armenia":"AM","Aruba":"AW","Australia":"AU","Austria":"AT","Azerbaijan":"AZ","Bahamas":"BS","Bahrain":"BH","Bangladesh":"BD","Barbados":"BB","Belarus":"BY","Belgium":"BE","Belize":"BZ","Benin":"BJ","Bermuda":"BM","Bhutan":"BT","Bolivia":"BO","Bonaire, Saint Eustatius and Saba ":"BQ","Bosnia and Herzegovina":"BA","Botswana":"BW","Bouvet Island":"BV","Brazil":"BR","British Indian Ocean Territory":"IO","British Virgin Islands":"VG","Brunei":"BN","Bulgaria":"BG","Burkina Faso":"BF","Burundi":"BI","Cambodia":"KH","Cameroon":"CM","Canada":"CA","Cape Verde":"CV","Cayman Islands":"KY","Central African Republic":"CF","Chad":"TD","Chile":"CL","China":"CN","Christmas Island":"CX","Cocos Islands":"CC","Colombia":"CO","Comoros":"KM","Cook Islands":"CK","Costa Rica":"CR","Croatia":"HR","Cuba":"CU","Curacao":"CW","Cyprus":"CY","Czech Republic":"CZ","Czechia":"CZ","Democratic Republic of the Congo":"CD","Denmark":"DK","Djibouti":"DJ","Dominica":"DM","Dominican Republic":"DO","East Timor":"TL","Ecuador":"EC","Egypt":"EG","El Salvador":"SV","Equatorial Guinea":"GQ","Eritrea":"ER","Estonia":"EE","Ethiopia":"ET","Falkland Islands":"FK","Faroe Islands":"FO","Fiji":"FJ","Finland":"FI","France":"FR","French Guiana":"GF","French Polynesia":"PF","French Southern Territories":"TF","Gabon":"GA","Gambia":"GM","Georgia":"GE","Germany":"DE","Ghana":"GH","Gibraltar":"GI","Greece":"GR","Greenland":"GL","Grenada":"GD","Guadeloupe":"GP","Guam":"GU","Guatemala":"GT","Guernsey":"GG","Guinea-Bissau":"GW","Guinea":"GN","Guyana":"GY","Haiti":"HT","Heard Island and McDonald Islands":"HM","Honduras":"HN","Hong Kong":"HK","Hungary":"HU","Iceland":"IS","India":"IN","Indonesia":"ID","Iran":"IR","Iraq":"IQ","Ireland":"IE","Isle of Man":"IM","Israel":"IL","Italy":"IT","Ivory Coast":"CI","Jamaica":"JM","Japan":"JP","Jersey":"JE","Jordan":"JO","Kazakhstan":"KZ","Kenya":"KE","Kiribati":"KI","Korea, South":"KR","Kosovo":"XK","Kuwait":"KW","Kyrgyzstan":"KG","Laos":"LA","Latvia":"LV","Lebanon":"LB","Lesotho":"LS","Liberia":"LR","Libya":"LY","Liechtenstein":"LI","Lithuania":"LT","Luxembourg":"LU","Macao":"MO","Macedonia":"MK","Madagascar":"MG","Mainland China":"CN","Malawi":"MW","Malaysia":"MY","Maldives":"MV","Mali":"ML","Malta":"MT","Marshall Islands":"MH","Martinique":"MQ","Mauritania":"MR","Mauritius":"MU","Mayotte":"YT","Mexico":"MX","Micronesia":"FM","Moldova":"MD","Monaco":"MC","Mongolia":"MN","Montenegro":"ME","Montserrat":"MS","Morocco":"MA","Mozambique":"MZ","Myanmar":"MM","Namibia":"NA","Nauru":"NR","Nepal":"NP","Netherlands":"NL","New Caledonia":"NC","New Zealand":"NZ","Nicaragua":"NI","Niger":"NE","Nigeria":"NG","Niue":"NU","Norfolk Island":"NF","North Korea":"KP","Northern Mariana Islands":"MP","Norway":"NO","Oman":"OM","Pakistan":"PK","Palau":"PW","Palestinian Territory":"PS","Panama":"PA","Papua New Guinea":"PG","Paraguay":"PY","Peru":"PE","Philippines":"PH","Pitcairn":"PN","Poland":"PL","Portugal":"PT","Puerto Rico":"PR","Qatar":"QA","Republic of the Congo":"CG","Reunion":"RE","Romania":"RO","Russia":"RU","Rwanda":"RW","Saint Barthelemy":"BL","Saint Helena":"SH","Saint Kitts and Nevis":"KN","Saint Lucia":"LC","Saint Martin":"MF","Saint Pierre and Miquelon":"PM","Saint Vincent and the Grenadines":"VC","Samoa":"WS","San Marino":"SM","Sao Tome and Principe":"ST","Saudi Arabia":"SA","Senegal":"SN","Serbia":"RS","Seychelles":"SC","Sierra Leone":"SL","Singapore":"SG","Sint Maarten":"SX","Slovakia":"SK","Slovenia":"SI","Solomon Islands":"SB","Somalia":"SO","South Africa":"ZA","South Georgia and the South Sandwich Islands":"GS","South Korea":"KR","South Sudan":"SS","Spain":"ES","Sri Lanka":"LK","Sudan":"SD","Suriname":"SR","Svalbard and Jan Mayen":"SJ","Swaziland":"SZ","Sweden":"SE","Switzerland":"CH","Syria":"SY","Taiwan":"TW","Taiwan*":"TW","Tajikistan":"TJ","Tanzania":"TZ","Thailand":"TH","Togo":"TG","Tokelau":"TK","Tonga":"TO","Trinidad and Tobago":"TT","Tunisia":"TN","Turkey":"TR","Turkmenistan":"TM","Turks and Caicos Islands":"TC","Tuvalu":"TV","U.S. Virgin Islands":"VI","Uganda":"UG","Ukraine":"UA","United Arab Emirates":"AE","United Kingdom":"GB","United States Minor Outlying Islands":"UM","Uruguay":"UY","US":"US","Uzbekistan":"UZ","Vanuatu":"VU","Vatican":"VA","Venezuela":"VE","Vietnam":"VN","Wallis and Futuna":"WF","Western Sahara":"EH","Yemen":"YE","Zambia":"ZM","Zimbabwe":"ZW"},"iso3":{"AD":"AND","AE":"ARE","AF":"AFG","AG":"ATG","AI":"AIA","AL":"ALB","AM":"ARM","AO":"AGO","AQ":"ATA","AR":"ARG","AS":"ASM","AT":"AUT","AU":"AUS","AW":"ABW","AX":"ALA","AZ":"AZE","BA":"BIH","BB":"BRB","BD":"BGD","BE":"BEL","BF":"BFA","BG":"BGR","BH":"BHR","BI":"BDI","BJ":"BEN","BL":"BLM","BM":"BMU","BN":"BRN","BO":"BOL","BQ":"BES","BR":"BRA","BS":"BHS","BT":"BTN","BV":"BVT","BW":"BWA","BY":"BLR","BZ":"BLZ","CA":"CAN","CC":"CCK","CD":"COD","CF":"CAF","CG":"COG","CH":"CHE","CI":"CIV","CK":"COK","CL":"CHL","CM":"CMR","CN":"CHN","CO":"COL","CR":"CRI","CU":"CUB","CV":"CPV","CW":"CUW","CX":"CXR","CY":"CYP","CZ":"CZE","DE":"DEU","DJ":"DJI","DK":"DNK","DM":"DMA","DO":"DOM","DZ":"DZA","EC":"ECU","EE":"EST","EG":"EGY","EH":"ESH","ER":"ERI","ES":"ESP","ET":"ETH","FI":"FIN","FJ":"FJI","FK":"FLK","FM":"FSM","FO":"FRO","FR":"FRA","GA":"GAB","GB":"GBR","GD":"GRD","GE":"GEO","GF":"GUF","GG":"GGY","GH":"GHA","GI":"GIB","GL":"GRL","GM":"GMB","GN":"GIN","GP":"GLP","GQ":"GNQ","GR":"GRC","GS":"SGS","GT":"GTM","GU":"GUM","GW":"GNB","GY":"GUY","HK":"HKG","HM":"HMD","HN":"HND","HR":"HRV","HT":"HTI","HU":"HUN","ID":"IDN","IE":"IRL","IL":"ISR","IM":"IMN","IN":"IND","IO":"IOT","IQ":"IRQ","IR":"IRN","IS":"ISL","IT":"ITA","JE":"JEY","JM":"JAM","JO":"JOR","JP":"JPN","KE":"KEN","KG":"KGZ","KH":"KHM","KI":"KIR","KM":"COM","KN":"KNA","KP":"PRK","KR":"KOR","KW":"KWT","KY":"CYM","KZ":"KAZ","LA":"LAO","LB":"LBN","LC":"LCA","LI":"LIE","LK":"LKA","LR":"LBR","LS":"LSO","LT":"LTU","LU":"LUX","LV":"LVA","LY":"LBY","MA":"MAR","MC":"MCO","MD":"MDA","ME":"MNE","MF":"MAF","MG":"MDG","MH":"MHL","MK":"MKD","ML":"MLI","MM":"MMR","MN":"MNG","MO":"MAC","MP":"MNP","MQ":"MTQ","MR":"MRT","MS":"MSR","MT":"MLT","MU":"MUS","MV":"MDV","MW":"MWI","MX":"MEX","MY":"MYS","MZ":"MOZ","NA":"NAM","NC":"NCL","NE":"NER","NF":"NFK","NG":"NGA","NI":"NIC","NL":"NLD","NO":"NOR","NP":"NPL","NR":"NRU","NU":"NIU","NZ":"NZL","OM":"OMN","PA":"PAN","PE":"PER","PF":"PYF","PG":"PNG","PH":"PHL","PK":"PAK","PL":"POL","PM":"SPM","PN":"PCN","PR":"PRI","PS":"PSE","PT":"PRT","PW":"PLW","PY":"PRY","QA":"QAT","RE":"REU","RO":"ROU","RS":"SRB","RU":"RUS","RW":"RWA","SA":"SAU","SB":"SLB","SC":"SYC","SD":"SDN","SE":"SWE","SG":"SGP","SH":"SHN","SI":"SVN","SJ":"SJM","SK":"SVK","SL":"SLE","SM":"SMR","SN":"SEN","SO":"SOM","SR":"SUR","SS":"SSD","ST":"STP","SV":"SLV","SX":"SXM","SY":"SYR","SZ":"SWZ","TC":"TCA","TD":"TCD","TF":"ATF","TG":"TGO","TH":"THA","TJ":"TJK","TK":"TKL","TL":"TLS","TM":"TKM","TN":"TUN","TO":"TON","TR":"TUR","TT":"TTO","TV":"TUV","TW":"TWN","TZ":"TZA","UA":"UKR","UG":"UGA","UM":"UMI","US":"USA","UY":"URY","UZ":"UZB","VA":"VAT","VC":"VCT","VE":"VEN","VG":"VGB","VI":"VIR","VN":"VNM","VU":"VUT","WF":"WLF","WS":"WSM","XK":"XKX","YE":"YEM","YT":"MYT","ZA":"ZAF","ZM":"ZMB","ZW":"ZWE"}} 2 | -------------------------------------------------------------------------------- /src/crawler.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from datetime import datetime 3 | 4 | from src.helper import DAERAH 5 | from src.db import session 6 | from src.models import Status 7 | 8 | ODI_API = 'https://indonesia-covid-19.mathdro.id/api/provinsi' 9 | 10 | 11 | def odi_api(state): 12 | req = requests.get(ODI_API) 13 | if not req.status_code == 200: 14 | return [] 15 | prov = {prov["provinsi"]: prov for prov in req.json()["data"]} 16 | hasil = prov[DAERAH[state]] 17 | todayIsNone = True 18 | 19 | result = { 20 | "tanggal": {"value": ""}, 21 | "total_sembuh": {"value": 0, "diff": 0}, 22 | "total_positif": {"value": 0, "diff": 0}, 23 | "total_meninggal": {"value": 0, "diff": 0}, 24 | "proses_pemantauan": {"value": 0}, 25 | "proses_pengawasan": {"value": 0}, 26 | "selesai_pemantauan": {"value": 0}, 27 | "selesai_pengawasan": {"value": 0}, 28 | "total_odp": {"value": 0}, 29 | "total_pdp": {"value": 0}, 30 | "source": {"value": ""} 31 | } 32 | 33 | result['metadata'] = { 34 | "source": "https://indonesia-covid-19.mathdro.id/", 35 | "province": DAERAH[state].upper() 36 | } 37 | 38 | get_state = session.query(Status) \ 39 | .filter(Status.country_id == f"id.{state}") \ 40 | .order_by(Status.created.desc()) \ 41 | .all() 42 | 43 | if len(get_state) > 0: 44 | for row in get_state: 45 | if not row.created.date() == datetime.utcnow().date(): 46 | result["total_sembuh"]["diff"] = \ 47 | hasil["kasusSemb"] - row.recovered 48 | result["total_positif"]["diff"] = \ 49 | hasil["kasusPosi"] - row.confirmed 50 | result["total_meninggal"]["diff"] = \ 51 | hasil["kasusMeni"] - row.deaths 52 | result["metadata"]["diff_date"] = \ 53 | row.created.isoformat() 54 | result["metadata"]["source_date"] = \ 55 | datetime.utcnow().isoformat() 56 | break 57 | else: 58 | todayIsNone = False 59 | result["metadata"]["source_date"] = \ 60 | row.created.isoformat() 61 | 62 | if todayIsNone: 63 | new_status = Status( 64 | confirmed=hasil["kasusPosi"], 65 | deaths=hasil["kasusMeni"], 66 | recovered=hasil["kasusSemb"], 67 | active_care=0, 68 | country_id=f"id.{state}", 69 | created=datetime.utcnow(), 70 | updated=datetime.utcnow() 71 | ) 72 | session.add(new_status) 73 | result["metadata"]["source_date"] = \ 74 | datetime.utcnow().isoformat() 75 | 76 | result["total_sembuh"]["value"] = hasil["kasusSemb"] 77 | result["total_positif"]["value"] = hasil["kasusPosi"] 78 | result["total_meninggal"]["value"] = hasil["kasusMeni"] 79 | 80 | return result 81 | -------------------------------------------------------------------------------- /src/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import scoped_session, sessionmaker 3 | from flask import g, _app_ctx_stack 4 | from src.models import Base 5 | from os import environ 6 | 7 | from src import config 8 | 9 | _ = environ.get 10 | 11 | session = scoped_session( 12 | sessionmaker(autocommit=False, autoflush=True, bind=None), 13 | scopefunc=_app_ctx_stack.__ident_func__, 14 | ) 15 | 16 | 17 | def init_app(app): 18 | app.before_request(_before_request) 19 | app.after_request(_after_request) 20 | app.teardown_appcontext(_teardown_appcontext) 21 | 22 | engine = create_engine(config.DBSTRING, echo=app.debug) 23 | session.configure(bind=engine) 24 | 25 | Base.query = session.query_property() 26 | 27 | 28 | def _before_request(): 29 | g.force_commit = False 30 | 31 | 32 | def _after_request(response): 33 | try: 34 | if getattr(g, 'force_commit', False) or response.status_code < 400: 35 | session.commit() 36 | else: 37 | session.rollback() 38 | finally: 39 | session.close() 40 | 41 | return response 42 | 43 | 44 | def _teardown_appcontext(exception=None): 45 | try: 46 | session.remove() 47 | except Exception: 48 | pass 49 | -------------------------------------------------------------------------------- /src/gobot-covid/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine AS builder 2 | 3 | # Install OS level dependencies 4 | RUN apk add --update alpine-sdk git && \ 5 | git config --global http.https://gopkg.in.followRedirects true 6 | 7 | WORKDIR /go/src/github.com/k1m0ch1/covid-19-api/ 8 | COPY ./ . 9 | 10 | RUN go build -o covid 11 | 12 | FROM alpine:3.8 13 | WORKDIR /go/src/github.com/k1m0ch1/covid-19-api/ 14 | COPY --from=builder /go/src/github.com/k1m0ch1/covid-19-api /go/src/github.com/k1m0ch1/covid-19-api 15 | COPY --from=builder /go/src/github.com/k1m0ch1/covid-19-api/covid /bin/covid 16 | 17 | ENTRYPOINT ["/bin/covid"] 18 | -------------------------------------------------------------------------------- /src/gobot-covid/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/k1m0ch1/covid-19-api 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/Rhymen/go-whatsapp v0.1.0 7 | github.com/golang/protobuf v1.3.5 // indirect 8 | github.com/gorilla/websocket v1.4.2 // indirect 9 | github.com/mdp/qrterminal/v3 v3.0.0 10 | github.com/pkg/errors v0.9.1 // indirect 11 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /src/gobot-covid/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= 2 | github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= 3 | github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= 4 | github.com/Rhymen/go-whatsapp v0.1.0 h1:XTXhFIQ/fx9jKObUnUX2Q+nh58EyeHNhX7DniE8xeuA= 5 | github.com/Rhymen/go-whatsapp v0.1.0/go.mod h1:xJSy+okeRjKkQEH/lEYrnekXB3PG33fqL0I6ncAkV50= 6 | github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME= 7 | github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU= 8 | github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw= 9 | github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM= 10 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= 12 | github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= 13 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 14 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 15 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 16 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 17 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 18 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 19 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 20 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 21 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 22 | github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= 23 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 24 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 25 | github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= 26 | github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= 27 | github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= 28 | github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= 29 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 30 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 32 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 33 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= 34 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= 35 | golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= 39 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 40 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 44 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 47 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 51 | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= 52 | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= 53 | -------------------------------------------------------------------------------- /src/gobot-covid/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "strings" 14 | "syscall" 15 | "time" 16 | "bytes" 17 | 18 | qrcodeTerminal "github.com/mdp/qrterminal/v3" 19 | whatsapp "github.com/Rhymen/go-whatsapp" 20 | ) 21 | 22 | var mainApi = "https://covid19-api.yggdrasil.id" 23 | var userAgent = "gobot-covid19/3.0" 24 | 25 | type waHandler struct { 26 | c *whatsapp.Conn 27 | startTime uint64 28 | } 29 | 30 | func (wh *waHandler) HandleTextMessage(message whatsapp.TextMessage) { 31 | if !strings.Contains(strings.ToLower(message.Text), "!covid") || message.Info.Timestamp < wh.startTime { 32 | return 33 | } 34 | 35 | reply := "timeout" 36 | waitSec := rand.Intn(4-2)+2 37 | param := "" 38 | log.Printf("Get message from %s, with message %s", message.Info.RemoteJid, message.Text) 39 | log.Printf("Randomly paused %d for throtling", waitSec) 40 | time.Sleep(time.Duration(waitSec) * time.Second) 41 | 42 | command := strings.Fields(message.Text) 43 | if len(command) > 1{ 44 | for index := range command[1:] { 45 | param += "/" + command[index+1] 46 | } 47 | } 48 | 49 | link := fmt.Sprintf(mainApi + "%s", param) 50 | 51 | body, err:= reqUrl(link) 52 | if err >= 400 { 53 | log.Printf("Error, %d when access %s ", err, link) 54 | return 55 | } 56 | 57 | var result map[string]interface {} 58 | 59 | jsonErr := json.Unmarshal(body, &result) 60 | if jsonErr != nil { 61 | log.Fatalf("Break point 4 %s %s", jsonErr, result) 62 | return 63 | } 64 | 65 | messages := result["message"] 66 | 67 | reply = messages.(string) 68 | 69 | if reply != "timeout" { 70 | go sendMessage(wh.c, reply, message.Info.RemoteJid) 71 | 72 | if _, ok := result["images"]; ok { 73 | images := result["images"].([]interface {}) 74 | for index := range(images){ 75 | go sendImage(wh.c, message.Info.RemoteJid, images[index].(string)) 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | 83 | func main() { 84 | wac, err := whatsapp.NewConn(5 * time.Second) 85 | wac.SetClientVersion(0, 4, 2080) 86 | if err != nil { 87 | log.Fatalf("error creating connection: %v\n", err) 88 | } 89 | 90 | wac.AddHandler(&waHandler{wac, uint64(time.Now().Unix())}) 91 | 92 | if err := login(wac); err != nil { 93 | log.Fatalf("error logging in: %v\n", err) 94 | } 95 | 96 | pong, err := wac.AdminTest() 97 | 98 | if !pong || err != nil { 99 | log.Fatalf("error pinging in: %v\n", err) 100 | } 101 | 102 | c := make(chan os.Signal, 1) 103 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 104 | <-c 105 | 106 | fmt.Println("Shutting down now.") 107 | session, err := wac.Disconnect() 108 | if err != nil { 109 | log.Fatalf("error disconnecting: %v\n", err) 110 | } 111 | if err := writeSession(session); err != nil { 112 | log.Fatalf("error saving session: %v", err) 113 | } 114 | } 115 | 116 | func sendMessage(wac *whatsapp.Conn, message string, RJID string) { 117 | msg := whatsapp.TextMessage{ 118 | Info: whatsapp.MessageInfo{ 119 | RemoteJid: RJID, 120 | }, 121 | Text: message, 122 | } 123 | 124 | msgId, err := wac.Send(msg) 125 | if err != nil { 126 | fmt.Fprintf(os.Stderr, "error sending message: %v", err) 127 | os.Exit(1) 128 | } else { 129 | fmt.Println("Message Sent -> ID : " + msgId) 130 | } 131 | } 132 | 133 | func sendImage(wac *whatsapp.Conn, RJID string, url string) { 134 | 135 | reqImg, err := http.Get(url) 136 | if err != nil { 137 | log.Fatalf("http.Get -> %v", err) 138 | } 139 | 140 | defer reqImg.Body.Close() 141 | 142 | img, err := ioutil.ReadAll(reqImg.Body) 143 | 144 | if err != nil { 145 | log.Fatalf("ioutil.ReadAll -> %v", err) 146 | } 147 | 148 | msg := whatsapp.ImageMessage{ 149 | Info: whatsapp.MessageInfo{ 150 | RemoteJid: RJID, 151 | }, 152 | Type: "image/jpeg", 153 | Caption: "Statistik dari kawalcovid19.co.id", 154 | Content: bytes.NewReader(img), 155 | } 156 | 157 | msgId,err := wac.Send(msg) 158 | if err != nil { 159 | fmt.Fprintf(os.Stderr, "error sending message: %v", err) 160 | os.Exit(1) 161 | } else { 162 | fmt.Println("Message Sent -> ID : "+msgId) 163 | } 164 | } 165 | 166 | func login(wac *whatsapp.Conn) error { 167 | //load saved session 168 | session, err := readSession() 169 | if err == nil { 170 | //restore session 171 | session, err = wac.RestoreWithSession(session) 172 | if err != nil { 173 | return fmt.Errorf("restoring failed: %v\n", err) 174 | } 175 | } else { 176 | //no saved session -> regular login 177 | qr := make(chan string) 178 | go func(){ 179 | config := qrcodeTerminal.Config{ 180 | Level: qrcodeTerminal.L, 181 | Writer: os.Stdout, 182 | BlackChar: qrcodeTerminal.BLACK, 183 | WhiteChar: qrcodeTerminal.WHITE, 184 | QuietZone: 1, 185 | } 186 | qrcodeTerminal.GenerateWithConfig(<-qr, config) 187 | }() 188 | session, err = wac.Login(qr) 189 | if err != nil { 190 | return fmt.Errorf("error during login: %v\n", err) 191 | } 192 | } 193 | 194 | //save session 195 | err = writeSession(session) 196 | if err != nil { 197 | return fmt.Errorf("error saving session: %v\n", err) 198 | } 199 | return nil 200 | } 201 | 202 | func readSession() (whatsapp.Session, error) { 203 | session := whatsapp.Session{} 204 | log.Println("Trying to get the session " + getSessionName()) 205 | file, err := os.Open(getSessionName()) 206 | if err != nil { 207 | return session, err 208 | } 209 | defer file.Close() 210 | decoder := gob.NewDecoder(file) 211 | err = decoder.Decode(&session) 212 | if err != nil { 213 | return session, err 214 | } 215 | return session, nil 216 | } 217 | 218 | func writeSession(session whatsapp.Session) error { 219 | file, err := os.Create(getSessionName()) 220 | if err != nil { 221 | return err 222 | } 223 | defer file.Close() 224 | encoder := gob.NewEncoder(file) 225 | err = encoder.Encode(session) 226 | if err != nil { 227 | return err 228 | } 229 | return nil 230 | } 231 | 232 | func getSessionName() string { 233 | mydir, err := os.Getwd() 234 | if err != nil { 235 | fmt.Println(err) 236 | } 237 | if _, err := os.Stat(mydir + "/session"); os.IsNotExist(err) { 238 | os.MkdirAll(mydir+"/session", os.ModePerm) 239 | } 240 | sessionName := "" 241 | if len(os.Args) == 1 { 242 | sessionName = mydir + "/session" + "/whatsappSession.gob" 243 | } else { 244 | sessionName = mydir + "/session/" + os.Args[1] + ".gob" 245 | } 246 | 247 | return sessionName 248 | } 249 | 250 | //HandleError needs to be implemented to be a valid WhatsApp handler 251 | func (h *waHandler) HandleError(err error) { 252 | if e, ok := err.(*whatsapp.ErrConnectionFailed); ok { 253 | log.Printf("Connection failed, underlying error: %v", e.Err) 254 | log.Println("Waiting 30sec...") 255 | <-time.After(30 * time.Second) 256 | log.Println("Reconnecting...") 257 | err := h.c.Restore() 258 | if err != nil { 259 | log.Fatalf("Restore failed: %v", err) 260 | } 261 | } else { 262 | log.Printf("error occoured: %v\n", err) 263 | } 264 | } 265 | 266 | func reqUrl(url string) ([]byte, int) { 267 | req, err := http.NewRequest("GET", url, nil) 268 | if err != nil { 269 | log.Fatalf("Break point 1 %s", err) 270 | } 271 | 272 | req.Header.Set("User-Agent", userAgent) 273 | 274 | client := &http.Client{} 275 | res, err := client.Do(req) 276 | if err != nil { 277 | log.Fatalln(err) 278 | } 279 | 280 | if res.StatusCode >= 400 { 281 | return nil, res.StatusCode 282 | } 283 | 284 | body, readErr := ioutil.ReadAll(res.Body) 285 | if readErr != nil { 286 | log.Fatalf("Break point 3 %s", readErr) 287 | } 288 | 289 | return body, 200 290 | } 291 | 292 | 293 | -------------------------------------------------------------------------------- /src/gunicorn_cfg.py: -------------------------------------------------------------------------------- 1 | import os 2 | port = os.environ.get('PORT', 5000) 3 | bind = f"0.0.0.0:{port}" 4 | 5 | # Copied from gunicorn.glogging.CONFIG_DEFAULTS 6 | logconfig_dict = { 7 | "root": {"level": "INFO", "handlers": ["console"]}, 8 | "loggers": { 9 | "gunicorn.error": { 10 | "propagate": True, 11 | }, 12 | "gunicorn.access": { 13 | "propagate": True, 14 | }, 15 | "app.app": { 16 | "propagate": False, 17 | } 18 | }, 19 | "handlers": { 20 | "console": { 21 | "class": "logging.StreamHandler", 22 | "formatter": "generic", 23 | "stream": "ext://sys.stdout" 24 | }, 25 | }, 26 | "formatters": { 27 | "generic": { 28 | "format": "[%(name)s] [%(process)s] [%(levelname)s] %(message)s", 29 | "class": "logging.Formatter" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/helper.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from datetime import datetime, timedelta 4 | from flask import request 5 | 6 | from src.db import session 7 | 8 | TODAY = datetime.utcnow().date() 9 | 10 | YESTERDAY_STR = { 11 | "slash": datetime.strftime(TODAY - timedelta(days=1), "%-m/%d/%y"), 12 | "hyphen": datetime.strftime(TODAY - timedelta(days=1), "%m-%d-%Y"), 13 | "hyphen-dmy": datetime.strftime(TODAY - timedelta(days=1), "%d-%m-%Y"), 14 | } 15 | 16 | TODAY_STR = { 17 | "slash": TODAY.strftime("%-m/%d/%y"), 18 | "hyphen": TODAY.strftime("%m-%d-%Y"), 19 | "hyphen-dmy": TODAY.strftime("%d-%m-%Y"), 20 | } 21 | 22 | 23 | def is_empty(string): 24 | return 0 if string == "" or string is None else string 25 | 26 | 27 | def is_bot(): 28 | return request.headers.get('User-Agent') == 'gobot-covid19/3.0' 29 | 30 | 31 | def is_not_bot(): 32 | return not request.headers.get('User-Agent') == 'gobot-covid19/2.0' 33 | 34 | 35 | def gen(number): 36 | return f"{int(number):,}".replace(",", ".") 37 | 38 | 39 | @contextlib.contextmanager 40 | def transaction(factory=session): 41 | """ 42 | Context manager for a transaction block. 43 | It is mean to be used outside of a request context. 44 | 45 | with transaction() as tx: 46 | tx.add(Object()) 47 | """ 48 | 49 | tx = factory() 50 | try: 51 | yield tx 52 | tx.commit() 53 | except BaseException: 54 | tx.rollback() 55 | raise 56 | finally: 57 | tx.close() 58 | 59 | 60 | DAERAH = { 61 | "aceh": "Aceh", 62 | "bali": "Bali", 63 | "banten": "Banten", 64 | "bengkulu": "Bengkulu", 65 | "gorontalo": "Gorontalo", 66 | "jakarta": "DKI Jakarta", 67 | "jambi": "Jambi", 68 | "jateng": "Jawa Tengah", 69 | "jatim": "Jawa Timur", 70 | "kalbar": "Kalimantan Barat", 71 | "kalsel": "Kalimantan Selatan", 72 | "kalteng": "Kalimantan Tengah", 73 | "kaltim": "Kalimantan Timur", 74 | "kaltara": "Kalimantan Utara", 75 | "kep-bangka": "Kepulauan Bangka Belitung", 76 | "kepri": "Kepulauan Riau", 77 | "lampung": "Lampung", 78 | "maluku": "Maluku", 79 | "maluku-utara": "Maluku Utara", 80 | "kalbar": "Kalimantan Barat", 81 | "sulbar": "Sulawesi Barat", 82 | "sulut": "Sulawesi Utara", 83 | "sulsel": "Sulawesi Selatan", 84 | "gorontalo": "Gorontalo", 85 | "ntb": "Nusa Tenggara Barat", 86 | "ntt": "Nusa Tenggara Timur", 87 | "papua": "Papua", 88 | "papua-barat": "Papua Barat", 89 | "riau": "Riau", 90 | "sulbar": "Sulawesi Barat", 91 | "sulsel": "Sulawesi Selatan", 92 | "sulteng": "Sulawesi Tengah", 93 | "sultra": "Sulawesi Tenggara", 94 | "sulut": "Sulawesi Utara", 95 | "sumbar": "Sumatera Barat", 96 | "sumsel": "Sumatera Selatan", 97 | "sumut": "Sumatera Utara", 98 | "yogya": "Daerah Istimewa Yogyakarta" 99 | } 100 | 101 | 102 | HOTLINE = [ 103 | { 104 | "kota": "Kabupaten Bandung", 105 | "callCenter": [], 106 | "hotline": ["082118219287"] 107 | }, 108 | { 109 | "kota": "Kabupaten Bandung Barat", 110 | "callCenter": [], 111 | "hotline": ["089522434611"] 112 | }, 113 | { 114 | "kota": "Kabupaten Bekasi", 115 | "callCenter": ["112", "119"], 116 | "hotline": ["02189910039", "08111139927", "085283980119"] 117 | }, 118 | { 119 | "kota": "Kabupaten Bogor", 120 | "callCenter": ["112", "119"], 121 | "hotline": [] 122 | }, 123 | { 124 | "kota": "Kabupaten Ciamis", 125 | "callCenter": ["119"], 126 | "hotline": ["081394489808", "085314993901"] 127 | }, 128 | { 129 | "kota": "Kabupaten Cianjur", 130 | "callCenter": [], 131 | "hotline": ["085321161119"] 132 | }, 133 | { 134 | "kota": "Kabupaten Cirebon", 135 | "callCenter": [], 136 | "hotline": ["02318800119", "081998800119"] 137 | }, 138 | { 139 | "kota": "Kabupaten Garut", 140 | "callCenter": ["119"], 141 | "hotline": ["02622802800", "08112040119"] 142 | }, 143 | { 144 | "kota": "Kabupaten Indramayu", 145 | "callCenter": [], 146 | "hotline": ["08111333314"] 147 | }, 148 | { 149 | "kota": "Kabupaten Karawang", 150 | "callCenter": ["119", "08999700119"], 151 | "hotline": ["085282537355", "082125569259", "081574371120"] 152 | }, 153 | { 154 | "kota": "Kabupaten Kuningan", 155 | "callCenter": [], 156 | "hotline": ["081388284346"] 157 | }, 158 | { 159 | "kota": "Kabupaten Majalengka", 160 | "callCenter": ["112"], 161 | "hotline": ["0233829111", "081324849727"] 162 | } 163 | , 164 | { 165 | "kota": "Kabupaten Pangandaran", 166 | "callCenter": ["119"], 167 | "hotline": ["085320643695"] 168 | } 169 | , 170 | { 171 | "kota": "Kabupaten Purwakarta", 172 | "callCenter": ["112"], 173 | "hotline": ["081909514472"] 174 | } 175 | , 176 | { 177 | "kota": "Kabupaten Subang", 178 | "callCenter": [], 179 | "hotline": ["081322916001", "082115467455"] 180 | } 181 | , 182 | { 183 | "kota": "Kabupaten Sukabumi", 184 | "callCenter": ["112"], 185 | "hotline": ["02666243816", "081213583160"] 186 | } 187 | , 188 | { 189 | "kota": "Kabupaten Sumedang", 190 | "callCenter": ["119"], 191 | "hotline": [] 192 | } 193 | , 194 | { 195 | "kota": "Kabupaten Tasikmalaya", 196 | "callCenter": ["119"], 197 | "hotline": ["08122066396"] 198 | } 199 | , 200 | { 201 | "kota": "Kota Bandung", 202 | "callCenter": ["112", "119"], 203 | "hotline": [] 204 | } 205 | , 206 | { 207 | "kota": "Kota Banjar", 208 | "callCenter": ["112"], 209 | "hotline": ["085223344119", "082120370313", "085353089099"] 210 | } 211 | , 212 | { 213 | "kota": "Kota Bekasi", 214 | "callCenter": ["119"], 215 | "hotline": ["081380027110"] 216 | } 217 | , 218 | { 219 | "kota": "Kota Bogor", 220 | "callCenter": ["112"], 221 | "hotline": ["02518363335", "08111116093"] 222 | } 223 | , 224 | { 225 | "kota": "Kota Cimahi", 226 | "callCenter": [], 227 | "hotline": ["08122126257", "081221423039"] 228 | } 229 | , 230 | { 231 | "kota": "Kota Cirebon", 232 | "callCenter": ["112"], 233 | "hotline": ["0231237303"] 234 | } 235 | , 236 | { 237 | "kota": "Kota Depok", 238 | "callCenter": ["112", "119"], 239 | "hotline": [] 240 | } 241 | , 242 | { 243 | "kota": "Kota Sukabumi", 244 | "callCenter": [], 245 | "hotline": ["08001000119"] 246 | } 247 | , 248 | { 249 | "kota": "Kota Tasikmalaya", 250 | "callCenter": [], 251 | "hotline": ["08112133119"] 252 | } 253 | ] 254 | -------------------------------------------------------------------------------- /src/limit.py: -------------------------------------------------------------------------------- 1 | from flask_limiter import Limiter 2 | 3 | limiter = Limiter() 4 | 5 | 6 | def init_app(app): 7 | limiter.init_app(app) 8 | -------------------------------------------------------------------------------- /src/models.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin 2 | from sqlalchemy import Column, MetaData, ForeignKey 3 | from sqlalchemy import String, Text, DateTime, Integer, Date 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy_utils import Timestamp, EmailType 6 | from sqlalchemy.sql.expression import text 7 | from sqlalchemy.orm import relationship 8 | import shortuuid 9 | 10 | NAMING_CONVENTION = { 11 | "ix": 'ix_%(column_0_label)s', 12 | "uq": "uq_%(table_name)s_%(column_0_name)s", 13 | "ck": "ck_%(table_name)s_%(constraint_name)s", 14 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 15 | "pk": "pk_%(table_name)s" 16 | } 17 | metadata = MetaData(naming_convention=NAMING_CONVENTION) 18 | Base = declarative_base(metadata=metadata, cls=Timestamp) 19 | 20 | 21 | def ServerTimestamp(cannull): 22 | return Column( 23 | DateTime(timezone=False), 24 | nullable=cannull, 25 | server_default=text("(now() at time zone 'utc')") 26 | ) 27 | 28 | 29 | class User(Base, UserMixin): 30 | __tablename__ = 'users' 31 | 32 | id = Column(String(22), primary_key=True, default=shortuuid.uuid) 33 | name = Column(String(100), unique=True) 34 | email = Column(EmailType, unique=True, nullable=True) 35 | birthdate = Column(Date, nullable=True) 36 | phone = Column(String(16), nullable=True) 37 | created = ServerTimestamp(False) 38 | updated = ServerTimestamp(True) 39 | 40 | def get_id(self): 41 | return self.id 42 | 43 | 44 | class Place(Base): 45 | __tablename__ = 'places' 46 | 47 | id = Column(String(22), primary_key=True, default=shortuuid.uuid) 48 | name = Column(Text, nullable=False) 49 | lng = Column(String(50), nullable=False) 50 | lat = Column(String(50), nullable=False) 51 | description = Column(Text, nullable=True) 52 | created = ServerTimestamp(False) 53 | updated = ServerTimestamp(True) 54 | 55 | story = relationship("Story") 56 | 57 | 58 | class Story(Base): 59 | __tablename__ = 'stories' 60 | 61 | id = Column(String(100), primary_key=True, default=shortuuid.uuid) 62 | place_id = Column(String(100), ForeignKey(Place.id), nullable=False) 63 | user_id = Column(String(100), ForeignKey(User.id), nullable=True) 64 | availability = Column(String(100), nullable=False) 65 | num = Column(Integer(), nullable=True) 66 | price = Column(Integer(), nullable=True) 67 | validity = Column(String(50), nullable=True, default="MASKER BIASA") 68 | created = ServerTimestamp(False) 69 | updated = ServerTimestamp(True) 70 | 71 | 72 | class Status(Base): 73 | __tablename__ = 'status' 74 | 75 | id = Column(Integer(), primary_key=True) 76 | confirmed = Column(Integer(), nullable=False) 77 | recovered = Column(Integer(), nullable=False) 78 | active_care = Column(Integer(), nullable=False) 79 | deaths = Column(Integer(), nullable=False) 80 | country_id = Column(String(3), nullable=False) 81 | created = ServerTimestamp(False) 82 | updated = ServerTimestamp(True) 83 | 84 | 85 | class Attachment(Base): 86 | __tablename__ = 'attachment' 87 | 88 | id = Column(String(22), primary_key=True, default=shortuuid.uuid) 89 | key = Column(String(70), nullable=True) 90 | name = Column(String(50), nullable=True) 91 | description = Column(Text, nullable=True) 92 | attachment = Column(Text, nullable=False) 93 | created = ServerTimestamp(False) 94 | updated = ServerTimestamp(True) 95 | -------------------------------------------------------------------------------- /src/provinces.json: -------------------------------------------------------------------------------- 1 | {"Provinces":{"aceh":{"full":"Aceh","sni":"AC"},"bali":{"full":"Bali","sni":"BA"},"banten":{"full":"Banten","sni":"BT"},"bengkulu":{"full":"Bengkulu","sni":"BE"},"gorontalo":{"full":"Gorontalo","sni":"GO"},"jakarta":{"full":"Daerah Khusus Ibukota Jakarta","sni":"JK"},"jambi":{"full":"Jambi","sni":"JA"},"jateng":{"full":"Jawa Tengah","sni":"JT"},"jatim":{"full":"Jawa Timur","sni":"JI"}},"kalbar":{"full":"Kalimantan Barat","sni":"KB"},"kalsel":{"full":"Kalimantan Selatan","sni":"KS"},"kalteng":{"full":"Kalimantan Tengah","sni":"KT"},"kaltim":{"full":"Kalimantan Timur","sni":"KI"},"kaltara":{"full":"Kalimantan Utara","sni":"KU"},"kep-bangka":{"full":"Kepulauan Bangka Belitung","sni":"BB"},"kepri":{"full":"Kepualauan Riau","sni":"KR"},"lampung":{"full":"Lampung","sni":"LA"},"maluku":{"full":"Maluku","sni":"MA"},"maluku-utara":{"full":"Maluku Utara","sni":"MU"},"ntb":{"full":"Nusa Tenggara Barat","sni":"NB"},"ntt":{"full":"Nusa Tenggara Timur","sni":"NT"},"papua":{"full":"Papua","sni":"PA"},"papua-barat":{"full":"Papua Barat","sni":"PB"},"riau":{"full":"Riau","sni":"RI"},"sulbar":{"full":"Sulawesi Barat","sni":"SR"},"sulsel":{"full":"Sulawesi Selatan","sni":"SN"},"sulteng":{"full":"Sulawesi Tengah","sni":"ST"},"sultra":{"full":"Sulawesi Tenggara","sni":"SG"},"sulut":{"full":"Sulawesi Utara","sni":"SA"},"sumbar":{"full":"Sumatra Barat","sni":"SB"},"sumsel":{"full":"Sumatra Selatan","sni":"SS"},"sumut":{"full":"Sumatra Utara","sni":"SU"},"yogya":{"full":"Daerah Istimewa Yogyakarta","sni":"YO"}} 2 | -------------------------------------------------------------------------------- /src/route/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .root import root 3 | from .maskmap import maskmap 4 | from .indonesia import indonesia 5 | 6 | 7 | def register_route(app: Flask): 8 | app.register_blueprint(root, url_prefix='/') 9 | app.register_blueprint(maskmap, url_prefix='/maskmap') 10 | app.register_blueprint(indonesia, url_prefix='/id') 11 | -------------------------------------------------------------------------------- /src/route/indonesia.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from wand.image import Image 3 | from flask import Blueprint, jsonify, Response, url_for 4 | from datetime import datetime, timedelta 5 | from dateutil import parser 6 | from base64 import b64decode 7 | 8 | from src.cache import cache 9 | from src.db import session 10 | from src.limit import limiter 11 | from src.models import Status, Attachment 12 | from src import seeder 13 | from src.helper import ( 14 | is_empty, is_bot, is_not_bot, 15 | TODAY_STR, TODAY, YESTERDAY_STR, 16 | DAERAH 17 | ) 18 | from src import bot, config, crawler 19 | 20 | indonesia = Blueprint('indonesia', __name__) 21 | JABAR = 'https://coredata.jabarprov.go.id/analytics/covid19/aggregation.json' 22 | 23 | KAWAL_COVID = "https://kawalcovid19.harippe.id/api/summary" 24 | CONTACT = "https://pikobar.jabarprov.go.id/contact/" 25 | DATASET_CONFIRMED = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv" # noqa 26 | DATASET_RECOVERED = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_recovered_global.csv" # noqa 27 | DATASET_DEATHS = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv" # noqa 28 | sLIMITER = config.sLIMITER 29 | 30 | 31 | @indonesia.route('/graph') 32 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 33 | @cache.cached(timeout=50) 34 | def graph(): 35 | get_latest = session.query(Attachment) \ 36 | .filter(Attachment.key == "graph.harian") \ 37 | .order_by(Attachment.created.desc()) \ 38 | .first() 39 | scr_png = b64decode(get_latest.attachment) 40 | scr_img = Image(blob=scr_png) 41 | return Response(scr_img.make_blob(), mimetype='image/jpeg') 42 | 43 | 44 | @indonesia.route('/seed') 45 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 46 | @cache.cached(timeout=50) 47 | def seed(): 48 | seeder.seed() 49 | 50 | 51 | @indonesia.route('/') 52 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 53 | @cache.cached(timeout=50) 54 | def id(): 55 | req = requests.get(KAWAL_COVID) 56 | data = req.json() 57 | updated = parser.parse(data['metadata']['lastUpdatedAt']) \ 58 | .replace(tzinfo=None) 59 | alldata = session.query(Status) \ 60 | .filter(Status.country_id == "id") \ 61 | .order_by(Status.created.desc()).all() 62 | dbDate = "" 63 | if len(alldata) > 0: 64 | getData = [_id_beauty(data, row) for row in alldata] 65 | dbDate = parser.parse(getData[0]["metadata"]["last_updated"]) \ 66 | .replace(tzinfo=None) 67 | if not updated == dbDate: 68 | new_status = Status( 69 | confirmed=data['confirmed']['value'], 70 | deaths=data['deaths']['value'], 71 | recovered=data['recovered']['value'], 72 | active_care=data['activeCare']['value'], 73 | country_id="id", 74 | created=updated, 75 | updated=updated 76 | ) 77 | session.add(new_status) 78 | for index, row in enumerate(getData): 79 | if not row['confirmed']['diff'] == 0 and \ 80 | not row['deaths']['diff'] == 0 and \ 81 | not row['recovered']['diff'] == 0 and \ 82 | not row['active_care']['diff'] == 0: 83 | row['metadata']['last_updated'] = \ 84 | getData[index-1]['metadata']['last_updated'] 85 | return _response(row, 200) 86 | return _response(getData[0], 200) 87 | else: 88 | new_status = Status( 89 | confirmed=data['confirmed']['value'], 90 | deaths=data['deaths']['value'], 91 | recovered=data['recovered']['value'], 92 | active_care=data['activeCare']['value'], 93 | country_id="id", 94 | created=updated, 95 | updated=updated 96 | ) 97 | session.add(new_status) 98 | return _response(_id_beauty(data, 0), 200) 99 | 100 | 101 | @indonesia.route('/jabar') 102 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 103 | @cache.cached(timeout=50) 104 | def jabar(): 105 | result = { 106 | "tanggal": {"value": ""}, 107 | "total_sembuh": {"value": 0, "diff": 0}, 108 | "total_positif": {"value": 0, "diff": 0}, 109 | "total_meninggal": {"value": 0, "diff": 0}, 110 | "proses_pemantauan": {"value": 0}, 111 | "proses_pengawasan": {"value": 0}, 112 | "selesai_pemantauan": {"value": 0}, 113 | "selesai_pengawasan": {"value": 0}, 114 | "total_odp": {"value": 0}, 115 | "total_pdp": {"value": 0}, 116 | "source": {"value": ""} 117 | } 118 | 119 | response = requests.get(JABAR) 120 | if not response.status_code == 200: 121 | jsonify({"message": f"Error when trying to crawl {JABAR}"}), 404 122 | json_resp = response.json() 123 | today_stat = _search_list(json_resp, 124 | "tanggal", TODAY_STR['hyphen-dmy']) 125 | yeday_stat = _search_list(json_resp, 126 | "tanggal", YESTERDAY_STR['hyphen-dmy']) 127 | 128 | if today_stat["selesai_pengawasan"] is None: 129 | twodaysago = _search_list( 130 | json_resp, "tanggal", 131 | datetime.strftime(TODAY - timedelta(days=2), "%d-%m-%Y")) 132 | result = _jabarset_value(yeday_stat, twodaysago) 133 | result['metadata'] = { 134 | "source": "https://pikobar.jabarprov.go.id", 135 | "province": "Jawa Barat", 136 | "source_date": YESTERDAY_STR['hyphen-dmy'], 137 | } 138 | else: 139 | result = _jabarset_value(today_stat, yeday_stat) 140 | result['metadata'] = { 141 | "source": "https://pikobar.jabarprov.go.id", 142 | "province": "Jawa Barat", 143 | "source_date": TODAY_STR['hyphen-dmy'], 144 | } 145 | 146 | if len(result) == 0: 147 | jsonify({"message": "Not Found"}), 404 148 | 149 | if is_bot(): 150 | return jsonify(message=bot.jabar(result)), 200 151 | else: 152 | return jsonify(result), 200 153 | 154 | 155 | @indonesia.route('/') 156 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 157 | @cache.cached(timeout=50) 158 | def province(province): 159 | if province in DAERAH: 160 | result = crawler.odi_api(province) 161 | if len(result) == 0: 162 | return jsonify(message="Not Found"), 404 163 | 164 | if is_bot(): 165 | return jsonify(message=bot.province(result)), 200 166 | else: 167 | return jsonify(result), 200 168 | 169 | if province == "prov" or province == "list": 170 | provinsi = [item for item in DAERAH] 171 | if is_bot(): 172 | return jsonify(message=bot.province_list(provinsi)), 200 173 | else: 174 | return jsonify([item for item in DAERAH]), 200 175 | return jsonify(message="Not Found"), 404 176 | 177 | 178 | @indonesia.errorhandler(429) 179 | def ratelimit_handler(e): 180 | return jsonify({"message": "Rate Limited"}), 429 181 | 182 | 183 | def _search_list(list, key, status_date): 184 | """ For List Search base on tanggal """ 185 | 186 | for l in list: 187 | if l[key] == status_date: 188 | return l 189 | 190 | 191 | def _jabarset_value(current, before): 192 | result = {} 193 | keys = [ 194 | 'meninggal', 'positif', 'proses_pemantauan', 'proses_pengawasan', 195 | 'selesai_pemantauan', 'selesai_pengawasan', 'sembuh', 196 | 'tanggal', 'total_meninggal', 'total_odp', 'total_pdp', 197 | 'total_positif_saat_ini', 'total_sembuh' 198 | ] 199 | for key in keys: 200 | if key not in \ 201 | ["tanggal", "meninggal", "positif", 202 | "selesai_pengawasan", "proses_pemantauan"]: 203 | result[key] = { 204 | "value": int(is_empty(current[key])), 205 | "diff": int(is_empty(current[key])) - 206 | int(is_empty(before[key])) 207 | } 208 | else: 209 | result[key] = {"value": int(is_empty(current[key])) 210 | if not key == "tanggal" else 211 | is_empty(current[key])} 212 | return result 213 | 214 | 215 | def _id_beauty(source, db): 216 | if db == 0: 217 | confirmed = 0 218 | deaths = 0 219 | recovered = 0 220 | active_care = 0 221 | updated = parser.parse(source['metadata']['lastUpdatedAt']) \ 222 | .replace(tzinfo=None) 223 | else: 224 | confirmed = db.confirmed 225 | deaths = db.deaths 226 | recovered = db.recovered 227 | active_care = db.active_care 228 | updated = db.created 229 | return { 230 | "confirmed": { 231 | "value": source['confirmed']['value'], 232 | "diff": source['confirmed']['value'] - confirmed 233 | }, 234 | "deaths": { 235 | "value": source['deaths']['value'], 236 | "diff": source['deaths']['value'] - deaths 237 | }, 238 | "recovered": { 239 | "value": source['recovered']['value'], 240 | "diff": source['recovered']['value'] - recovered 241 | }, 242 | "active_care": { 243 | "value": source['activeCare']['value'], 244 | "diff": source['activeCare']['value'] - active_care 245 | }, 246 | "metadata": { 247 | "country_id": "id", 248 | "last_updated": updated.isoformat() 249 | } 250 | } 251 | 252 | 253 | def _response(data, responseCode): 254 | if is_bot(): 255 | return jsonify( 256 | message=bot.id(data), 257 | images=[url_for('indonesia.graph', _external=True)]), responseCode 258 | else: 259 | return jsonify(data), responseCode 260 | 261 | -------------------------------------------------------------------------------- /src/route/maskmap.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | from src.cache import cache 3 | import time 4 | from sqlalchemy import or_ 5 | from datetime import datetime, timedelta 6 | 7 | from src.db import session 8 | from src.models import User, Place, Story 9 | from src.helper import is_bot 10 | 11 | 12 | maskmap = Blueprint('maskmap', __name__) 13 | 14 | 15 | @maskmap.before_request 16 | def before_request(request): 17 | if is_bot(): 18 | return jsonify(message="Not Found"), 404 19 | 20 | 21 | @maskmap.after_request 22 | def after_request(response): 23 | response.headers.add('Access-Control-Allow-Origin', '*') 24 | response.headers.add( 25 | 'Access-Control-Allow-Headers', 'Content-Type,Authorization') 26 | response.headers.add('Access-Control-Allow-Methods', 'POST') 27 | return response 28 | 29 | 30 | @maskmap.route('/places', methods=['POST']) 31 | @cache.cached(timeout=50) 32 | def queryPlaces(): 33 | body = request.get_json() 34 | places = session.query(Place) \ 35 | .filter(or_(Place.name.like(f'%{body.get("name")}%'), 36 | Place.lng == body.get("long"), 37 | Place.lat == body.get("lat")) 38 | ) \ 39 | .all() 40 | 41 | if len(places) == 0: 42 | return jsonify(message="Not Found"), 404 43 | 44 | if places is not None: 45 | return jsonify([_placeDict(place) for place in places]), 200 46 | 47 | 48 | @maskmap.route('/places') 49 | @cache.cached(timeout=50) 50 | def getAllPlaces(): 51 | diff = (datetime.utcnow() - timedelta(days=2)) 52 | if request.args.get("days"): 53 | diff = (datetime.utcnow() - 54 | timedelta(days=int(request.args.get("days")))) 55 | if request.args.get("hours"): 56 | diff = (datetime.utcnow() - 57 | timedelta(days=int(request.args.get("hours")))) 58 | 59 | places = session.query(Place).all() 60 | 61 | if len(places) == 0: 62 | return jsonify(message="Not Found"), 404 63 | 64 | if places is not None: 65 | return jsonify([_placeDict(place) for place in places]), 200 66 | 67 | 68 | @maskmap.route('/stories') 69 | @cache.cached(timeout=50) 70 | def getAllStories(): 71 | stories = session.query(Story).all() 72 | 73 | if len(stories) == 0: 74 | return jsonify(message="Not Found"), 404 75 | 76 | if stories is not None: 77 | return jsonify([_storyDict(story) for story in stories]), 200 78 | 79 | 80 | @maskmap.route('/story/') 81 | def getStory(story_id): 82 | story = session.query(Story).get(story_id) 83 | if story is not None: 84 | return jsonify(_storyDict(story)), 200 85 | return jsonify(message="Not Found"), 404 86 | 87 | 88 | @maskmap.route('/place/') 89 | def getPlace(place_id): 90 | place = session.query(Place).get(place_id) 91 | if place is not None: 92 | return jsonify(_placeDict(place)), 200 93 | return jsonify(message="Not Found"), 404 94 | 95 | 96 | @maskmap.route('/user/') 97 | def getUser(user_id): 98 | user = session.query(User).get(user_id) 99 | if user is not None: 100 | return jsonify({ 101 | "id": user.id, 102 | "name": user.name, 103 | "email": user.email, 104 | "birthdate": time.mktime(user.birthdate.timetuple()), 105 | "phone": user.phone, 106 | "created": time.mktime(user.created.timetuple()) 107 | }), 200 108 | return jsonify(message="Not Found"), 404 109 | 110 | 111 | @maskmap.route('/user', methods=['POST']) 112 | def handleUserCreated(): 113 | body = request.get_json() 114 | new_user = User( 115 | name=body.get("name"), 116 | email=body.get("email"), 117 | birthdate=body.get("birthdate"), 118 | phone=body.get("phone"), 119 | ) 120 | 121 | session.add(new_user) 122 | session.flush() 123 | 124 | return jsonify({'id': new_user.id}) 125 | 126 | 127 | @maskmap.route('/place', methods=['POST']) 128 | def handlePlaceCreated(): 129 | body = request.get_json() 130 | new_place = Place( 131 | name=body.get("name"), 132 | lng=body.get("long"), 133 | lat=body.get("lat"), 134 | description=body.get("description"), 135 | ) 136 | 137 | session.add(new_place) 138 | session.flush() 139 | 140 | return jsonify({'id': new_place.id}) 141 | 142 | 143 | @maskmap.route('/stories', methods=['POST']) 144 | def handleStoriesCreated(): 145 | body = request.get_json() 146 | new_story = Story( 147 | place_id=body.get("place_id"), 148 | user_id=body.get("user_id"), 149 | availability=body.get("availability"), 150 | num=body.get("num"), 151 | price=body.get("price"), 152 | validity=body.get("validity") 153 | ) 154 | 155 | session.add(new_story) 156 | session.flush() 157 | 158 | return jsonify({'id': new_story.id}) 159 | 160 | 161 | def _placeDict(place): 162 | return { 163 | "id": place.id, 164 | "name": place.name, 165 | "lat": place.lat, 166 | "long": place.lng, 167 | "description": place.description, 168 | "created": place.created.isoformat(), 169 | "availability": _groupSort(place.story), 170 | "story": [_storyDict(row) for row in place.story] 171 | } 172 | 173 | 174 | def _storyDict(story): 175 | return { 176 | "id": story.id, 177 | "place_id": story.place_id, 178 | "user_id": story.user_id, 179 | "availability": story.availability, 180 | "num": story.num, 181 | "price": story.price, 182 | "validity": story.validity, 183 | "created": time.mktime(story.created.timetuple()) 184 | } 185 | 186 | 187 | def _groupSort(story): 188 | data = {} 189 | for row in story: 190 | if row.validity not in data: 191 | data[row.validity] = {} 192 | 193 | if row.availability not in data[row.validity]: 194 | data[row.validity][row.availability] = 0 195 | 196 | data[row.validity][row.availability] += 1 197 | return data 198 | -------------------------------------------------------------------------------- /src/route/root.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from src.cache import cache 3 | from datetime import datetime, timedelta 4 | from dateutil import parser 5 | 6 | import json 7 | import csv 8 | import requests 9 | import re 10 | 11 | from src.helper import ( 12 | is_bot, is_not_bot, 13 | TODAY_STR, YESTERDAY_STR, TODAY, 14 | HOTLINE 15 | ) 16 | 17 | from src.config import ( 18 | sLIMITER, DATASET_ALL, 19 | NEWSAPI_HOST, NEWSAPI_KEY 20 | ) 21 | from src.limit import limiter 22 | from src import bot 23 | 24 | root = Blueprint('root', __name__) 25 | 26 | DEFAULT_KEYS = ['Confirmed', 'Deaths', 'Recovered'] 27 | 28 | 29 | @root.route('/') 30 | @root.route('/status') 31 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 32 | @cache.cached(timeout=50) 33 | def index(): 34 | data = { 35 | "confirmed": 0, 36 | "deaths": 0, 37 | "recovered": 0, 38 | } 39 | 40 | for item in _get_today(): 41 | data['confirmed'] += int(item['confirmed']) 42 | data['deaths'] += int(item['deaths']) 43 | data['recovered'] += int(item['recovered']) 44 | 45 | if is_bot(): 46 | return jsonify(message=bot.summary(data)), 200 47 | return jsonify(data), 200 48 | 49 | 50 | @root.route('/hotline') 51 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 52 | @cache.cached(timeout=50) 53 | def hotline(): 54 | if is_bot(): 55 | return jsonify(message=bot.hotline(HOTLINE)), 200 56 | return jsonify(HOTLINE), 200 57 | 58 | 59 | @root.route('/hotline/') 60 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 61 | @cache.cached(timeout=50) 62 | def hot_line(state): 63 | results = [] 64 | for city in HOTLINE: 65 | key = city["kota"].lower() 66 | if re.search(r"\b%s" % state.lower(), key): 67 | results.append(city) 68 | if is_bot(): 69 | return jsonify(message=bot.hotline(results)), 200 70 | return jsonify(results) 71 | 72 | 73 | @root.route('/') 74 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 75 | @cache.cached(timeout=50) 76 | def stat(status): 77 | if is_bot(): 78 | return jsonify(message="Not Found"), 404 79 | if status in ['confirmed', 'deaths', 'recovered']: 80 | return jsonify(_get_today(only_keys=status)), 200 81 | return jsonify(message="Not Found"), 404 82 | 83 | 84 | @root.route('/provinces') 85 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 86 | @cache.cached(timeout=50) 87 | def provinces(): 88 | if is_bot(): 89 | return jsonify(message="Not Found"), 404 90 | with open('src/provinces.json', 'rb') as outfile: 91 | return jsonify(json.load(outfile)), 200 92 | return jsonify({"message": "Error Occured"}), 500 93 | 94 | 95 | @root.route('/countries') 96 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 97 | @cache.cached(timeout=50) 98 | def countries(): 99 | if is_bot(): 100 | return jsonify(message="Not Found"), 404 101 | with open('src/countries.json', 'rb') as outfile: 102 | return jsonify(json.load(outfile)), 200 103 | return jsonify({"message": "Error Occured"}), 500 104 | 105 | 106 | @root.route('/status/') 107 | @root.route('/countries/') 108 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 109 | @cache.cached(timeout=50) 110 | def country(country_id): 111 | if is_bot(): 112 | return jsonify( 113 | message=bot.countries( 114 | _get_today(country=country_id.upper(), mode="diff"))), 200 115 | return jsonify(_get_today(country=country_id.upper())), 200 116 | 117 | 118 | @root.route('/news/') 119 | @root.route('/news') 120 | @limiter.limit(f"1/{sLIMITER}second", key_func=lambda: is_bot(), exempt_when=lambda: is_not_bot()) # noqa 121 | @cache.cached(timeout=50) 122 | def news(**kwargs): 123 | if NEWSAPI_KEY == "xxxxxxxxxx": 124 | return jsonify({"message": "Please provide the NewsApi API KEY"}), 500 125 | 126 | param = {"apiKey": NEWSAPI_KEY, "q": "corona", 127 | "pageSize": 3, "sources": "bbc-news"} 128 | 129 | if "countries" in kwargs: 130 | param["country"] = kwargs['countries'] 131 | param.pop("sources") 132 | 133 | req = requests.get(NEWSAPI_HOST, params=param) 134 | 135 | if not req.status_code == 200: 136 | return jsonify({"message": "Error occured while " 137 | "trying to get the news"}), req.status_code 138 | 139 | if is_bot(): 140 | return jsonify(message=bot.news(req.json()['articles'])), 200 141 | return jsonify(req.json()['articles']), 200 142 | 143 | 144 | @root.route('/all') 145 | @cache.cached(timeout=50) 146 | def all_data(): 147 | return jsonify(_get_today()), 200 148 | 149 | 150 | @root.route('/hi') 151 | @root.route('/hello') 152 | @root.route('/halo') 153 | @root.route('/help') 154 | @root.route('/hola') 155 | @cache.cached(timeout=50) 156 | def introduction(): 157 | if is_bot(): 158 | return jsonify(message=bot.introduction()), 200 159 | return jsonify(message="Not Found"), 404 160 | 161 | 162 | def _get_today(**kwargs): 163 | now_data = TODAY_STR['hyphen'] 164 | prev_data = YESTERDAY_STR['hyphen'] 165 | get_data = _extract_handler(DATASET_ALL % TODAY_STR['hyphen']) 166 | 167 | if not get_data: 168 | now_data = YESTERDAY_STR['hyphen'] 169 | prev_data = datetime.strftime(TODAY - timedelta(days=2), "%m-%d-%Y") 170 | get_data = _extract_handler(DATASET_ALL % now_data) 171 | 172 | result = [{f"{re.sub('[_]', '', key)[0].lower()}" 173 | f"{re.sub('[_]', '', key)[1:]}": 174 | int(item[key]) if key in DEFAULT_KEYS else 175 | None if item[key] == "" else item[key] for key in item} 176 | for item in get_data] 177 | 178 | if "only_keys" in kwargs: 179 | basic_keys = ["confirmed", "deaths", "recovered"] 180 | basic_keys.pop(basic_keys.index(kwargs['only_keys'])) 181 | result = [] 182 | curr_index = 0 183 | for item in get_data: 184 | for key in item: 185 | if key.lower() in basic_keys: 186 | continue 187 | result.append({}) 188 | 189 | curr_key = f"{re.sub('[_]', '', key)[0].lower()}" \ 190 | f"{re.sub('[_]', '', key)[1:]}" 191 | 192 | result[curr_index][curr_key] = \ 193 | int(item[key]) if key in DEFAULT_KEYS else \ 194 | None if item[key] == "" else item[key] 195 | curr_index += 1 196 | 197 | if "country" in kwargs: 198 | result = _country(kwargs['country'], get_data) 199 | 200 | if "mode" in kwargs: 201 | if kwargs['mode'] == "diff": 202 | result = { 203 | "origin": result, 204 | "diff": "", 205 | "last_updated": parser.parse(now_data).isoformat() 206 | } 207 | get_prev_data = _extract_handler(DATASET_ALL % prev_data) 208 | result['diff'] = _country(kwargs['country'], get_prev_data) 209 | 210 | return result 211 | 212 | 213 | def _extract_handler(url): 214 | request = requests.get(url) 215 | 216 | if not request.status_code == 200: 217 | return False 218 | 219 | return [item for item in csv.DictReader(request.text.splitlines())] 220 | 221 | 222 | def _country(negara, data): 223 | countries = None 224 | key_country = "" 225 | result = [] 226 | with open('src/countries.json', 'rb') as outfile: 227 | countries = json.load(outfile) 228 | 229 | for country in countries["countries"]: 230 | if countries["countries"][country] == negara: 231 | key_country = country 232 | break 233 | for item in data: 234 | if item["Country_Region"] == key_country: 235 | result.append({}) 236 | for key in item: 237 | curr_key = f"{re.sub('[_]', '', key)[0].lower()}" \ 238 | f"{re.sub('[_]', '', key)[1:]}" 239 | 240 | result[-1][curr_key] = \ 241 | int(item[key]) if key in DEFAULT_KEYS else \ 242 | None if item[key] == "" else item[key] 243 | 244 | return result 245 | -------------------------------------------------------------------------------- /src/seeder.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode, b64encode 2 | from wand.image import Image 3 | from selenium import webdriver 4 | from selenium.webdriver.common.action_chains import ActionChains 5 | from datetime import datetime 6 | 7 | from src import config, helper 8 | from src.models import Attachment, Status 9 | 10 | import time 11 | import math 12 | import requests 13 | 14 | 15 | def seed(): 16 | # seed the statistic graph 17 | print("[*] Start Seeder") 18 | time.sleep(2) 19 | print("--[+] Seed Graph") 20 | options = webdriver.ChromeOptions() 21 | options.headless = True 22 | options.add_argument("--no-sandbox") 23 | options.add_argument("--disable-gpu") 24 | options.add_argument("--disable-software-rasterizer") 25 | options.add_argument("--disable-dev-shm-usage") 26 | driver = webdriver.Chrome(config.CHROMEDRIVER, options=options) 27 | driver.get(config.STATISTIK_URL) 28 | time.sleep(15) 29 | driver.set_window_size(1600, 1200) 30 | image = driver.find_element_by_tag_name('body') 31 | ActionChains(driver).move_to_element(image).perform() 32 | src_base64 = driver.get_screenshot_as_base64() 33 | scr_png = b64decode(src_base64) 34 | scr_img = Image(blob=scr_png) 35 | 36 | x = image.location["x"] 37 | y = image.location["y"] + 510 38 | w = image.size["width"] - 720 39 | h = image.size["height"] - 757 40 | scr_img.crop( 41 | left=math.floor(x), 42 | top=math.floor(y), 43 | width=math.ceil(w), 44 | height=math.ceil(h), 45 | ) 46 | 47 | encoded_data = b64encode(scr_img.make_blob()).decode("utf-8") 48 | print("--[+] Save Graph to DB") 49 | with helper.transaction() as tx: 50 | save_attachment = Attachment( 51 | key="graph.harian", 52 | attachment=encoded_data 53 | ) 54 | 55 | tx.add(save_attachment) 56 | 57 | print("--[+] Start Seed Province") 58 | # seed the province 59 | for province in helper.DAERAH: 60 | print(f"--[+] Seed {province}") 61 | nothing = odi_api(province) # noqa 62 | print("[*] Seeder Done") 63 | 64 | 65 | ODI_API = 'https://indonesia-covid-19.mathdro.id/api/provinsi' 66 | 67 | 68 | def odi_api(state): 69 | DAERAH = helper.DAERAH 70 | req = requests.get(ODI_API) 71 | if not req.status_code == 200: 72 | return [] 73 | prov = {prov["provinsi"]: prov for prov in req.json()["data"]} 74 | hasil = prov[DAERAH[state]] 75 | todayIsNone = True 76 | 77 | result = { 78 | "tanggal": {"value": ""}, 79 | "total_sembuh": {"value": 0, "diff": 0}, 80 | "total_positif": {"value": 0, "diff": 0}, 81 | "total_meninggal": {"value": 0, "diff": 0}, 82 | "proses_pemantauan": {"value": 0}, 83 | "proses_pengawasan": {"value": 0}, 84 | "selesai_pemantauan": {"value": 0}, 85 | "selesai_pengawasan": {"value": 0}, 86 | "total_odp": {"value": 0}, 87 | "total_pdp": {"value": 0}, 88 | "source": {"value": ""} 89 | } 90 | 91 | result['metadata'] = { 92 | "source": "https://indonesia-covid-19.mathdro.id/", 93 | "province": DAERAH[state].upper() 94 | } 95 | 96 | with helper.transaction() as tx: 97 | get_state = tx.query(Status) \ 98 | .filter(Status.country_id == f"id.{state}") \ 99 | .order_by(Status.created.desc()) \ 100 | .all() 101 | 102 | if len(get_state) > 0: 103 | for row in get_state: 104 | if not row.created.date() == datetime.utcnow().date(): 105 | result["total_sembuh"]["diff"] = \ 106 | hasil["kasusSemb"] - row.recovered 107 | result["total_positif"]["diff"] = \ 108 | hasil["kasusPosi"] - row.confirmed 109 | result["total_meninggal"]["diff"] = \ 110 | hasil["kasusMeni"] - row.deaths 111 | result["metadata"]["diff_date"] = \ 112 | row.created.isoformat() 113 | result["metadata"]["source_date"] = \ 114 | datetime.utcnow().isoformat() 115 | break 116 | else: 117 | todayIsNone = False 118 | result["metadata"]["source_date"] = \ 119 | row.created.isoformat() 120 | 121 | if todayIsNone: 122 | new_status = Status( 123 | confirmed=hasil["kasusPosi"], 124 | deaths=hasil["kasusMeni"], 125 | recovered=hasil["kasusSemb"], 126 | active_care=0, 127 | country_id=f"id.{state}", 128 | created=datetime.utcnow(), 129 | updated=datetime.utcnow() 130 | ) 131 | tx.add(new_status) 132 | result["metadata"]["source_date"] = \ 133 | datetime.utcnow().isoformat() 134 | 135 | result["total_sembuh"]["value"] = hasil["kasusSemb"] 136 | result["total_positif"]["value"] = hasil["kasusPosi"] 137 | result["total_meninggal"]["value"] = hasil["kasusMeni"] 138 | 139 | return result 140 | -------------------------------------------------------------------------------- /src/wsgi.py: -------------------------------------------------------------------------------- 1 | from .app import create_app 2 | 3 | app = create_app() 4 | --------------------------------------------------------------------------------