From e3e02fda8a6de55f6811e01679d661730b1e827a Mon Sep 17 00:00:00 2001 From: misodengaku Date: Thu, 16 Nov 2023 17:39:59 +0900 Subject: [PATCH] =?UTF-8?q?python:=20=E8=A6=8B=E3=81=9F=E7=9B=AE=E3=81=AE?= =?UTF-8?q?=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * type hint * dictベタ書きだったのをdataclassに変更 * fill_*でNone返す代わりにHttpException起こす * 設定値の定義/取得処理をSettingsクラスにまとめた * HTTPレスポンスコード定義はhttp.clientを参照するように変更し、requestsへの依存を除去した --- webapp/python/Dockerfile | 1 + webapp/python/Pipfile | 1 - webapp/python/Pipfile.lock | 174 +----- webapp/python/app.py | 1169 +++++++++++++++++++++--------------- webapp/python/models.py | 162 +++++ 5 files changed, 884 insertions(+), 623 deletions(-) create mode 100644 webapp/python/models.py diff --git a/webapp/python/Dockerfile b/webapp/python/Dockerfile index 43c67e7ec..64271522f 100644 --- a/webapp/python/Dockerfile +++ b/webapp/python/Dockerfile @@ -19,6 +19,7 @@ WORKDIR /home/isucon/webapp/python COPY --chown=isucon:isucon Pipfile /home/isucon/webapp/python/ COPY --chown=isucon:isucon Pipfile.lock /home/isucon/webapp/python/ RUN pipenv install +COPY --chown=isucon:isucon models.py /home/isucon/webapp/python/ COPY --chown=isucon:isucon app.py /home/isucon/webapp/python/ # ENV GOPATH=/home/isucon/tmp/go diff --git a/webapp/python/Pipfile b/webapp/python/Pipfile index 493263bef..24648ebf2 100644 --- a/webapp/python/Pipfile +++ b/webapp/python/Pipfile @@ -6,7 +6,6 @@ name = "pypi" [packages] flask = "*" pymysql = "*" -requests = "*" bcrypt = "*" [dev-packages] diff --git a/webapp/python/Pipfile.lock b/webapp/python/Pipfile.lock index ab8c2caa1..8ac92865b 100644 --- a/webapp/python/Pipfile.lock +++ b/webapp/python/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "569586c461308e06e7265ae6ab4608378e7c4907cdb38eb32f0ac7460abba63c" + "sha256": "d4bd25e744c0103c4ec67f0b46b74c4b91f78f125f7411b83ffab6dbb0125e2c" }, "pipfile-spec": 6, "requires": { @@ -52,110 +52,6 @@ "markers": "python_version >= '3.8'", "version": "==1.7.0" }, - "certifi": { - "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.7.22" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, "click": { "hashes": [ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", @@ -172,14 +68,6 @@ "index": "pypi", "version": "==3.0.0" }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, "itsdangerous": { "hashes": [ "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", @@ -270,22 +158,6 @@ "index": "pypi", "version": "==1.1.0" }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "index": "pypi", - "version": "==2.31.0" - }, - "urllib3": { - "hashes": [ - "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", - "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.7" - }, "werkzeug": { "hashes": [ "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", @@ -298,27 +170,27 @@ "develop": { "black": { "hashes": [ - "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884", - "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916", - "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258", - "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1", - "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce", - "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d", - "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982", - "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7", - "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173", - "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9", - "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb", - "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad", - "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc", - "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0", - "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a", - "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe", - "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace", - "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69" + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", - "version": "==23.10.1" + "version": "==23.11.0" }, "click": { "hashes": [ @@ -378,11 +250,11 @@ }, "platformdirs": { "hashes": [ - "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "version": "==4.0.0" }, "pycodestyle": { "hashes": [ diff --git a/webapp/python/app.py b/webapp/python/app.py index 14f74c2f4..8433efecf 100644 --- a/webapp/python/app.py +++ b/webapp/python/app.py @@ -6,15 +6,41 @@ import subprocess import uuid from base64 import b64decode +from dataclasses import asdict from datetime import datetime, timedelta, timezone +from http.client import ( + BAD_REQUEST, + CREATED, + FORBIDDEN, + INTERNAL_SERVER_ERROR, + NOT_FOUND, + OK, + UNAUTHORIZED, +) +from typing import Any import bcrypt import flask +import models import pymysql.cursors -import requests -class Constants(object): +class Settings(object): + DB_HOST = os.getenv("ISUCON13_MYSQL_DIALCONFIG_ADDRESS", "127.0.0.1") + DB_PORT = int(os.getenv("ISUCON13_MYSQL_DIALCONFIG_PORT", 3306)) + DB_USER = os.getenv("ISUCON13_MYSQL_DIALCONFIG_USER", "isucon") + DB_PASSWORD = os.getenv("ISUCON13_MYSQL_DIALCONFIG_PASSWORD", "isucon") + DB_NAME = os.getenv("ISUCON13_MYSQL_DIALCONFIG_DATABASE", "isupipe") + + POWERDNS_ENABLED = os.getenv("ISUCON13_POWERDNS_DISABLED") != "true" + POWERDNS_SUBDOMAIN_ADDRESS = os.getenv("ISUCON13_POWERDNS_SUBDOMAIN_ADDRESS") + + APP_SECRET_KEY = "ISUPIPE_SUPER_SECRET_KEY" + + SESSION_COOKIE_DOMAIN = "u.isucon.dev" + SESSION_COOKIE_PATH = "/" + PERMANENT_SESSION_LIFETIME = 600 # 10min + DEFAULT_SESSION_ID_KEY = "SESSIONID" DEFAULT_SESSION_EXPIRES_KEY = "EXPIRES" DEFAULT_USER_ID_KEY = "USERID" @@ -22,88 +48,98 @@ class Constants(object): app = flask.Flask(__name__) -app.secret_key = "9pwSq4nJ9sg4HeSY6ZRe" -app.config["SESSION_COOKIE_DOMAIN"] = "u.isucon.dev" -app.config["SESSION_COOKIE_PATH"] = "/" -app.config["PERMANENT_SESSION_LIFETIME"] = 600 # 10min # 初期化 @app.route("/api/initialize", methods=["POST"]) -def initialize_handler(): +def initialize_handler() -> tuple[dict[str, Any], int]: app.logger.info("start initialize") result = subprocess.run(["../sql/init.sh"], capture_output=True, text=True) if result.returncode != 0: app.logger.error("init.sh failed with err=", result.stdout) - raise HttpException(result.stderr, 500) + raise HttpException(result.stderr, INTERNAL_SERVER_ERROR) - return {"advertise_level": 10, "language": "python"} + return {"advertise_level": 10, "language": "python"}, OK # top @app.route("/api/tag", methods=["GET"]) -def get_tag_handler(): +def get_tag_handler() -> tuple[dict[str, Any], int]: try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM tags" c.execute(sql) - tags = c.fetchall() + rows = c.fetchall() + if rows is None: + raise HttpException("failed to get tags", INTERNAL_SERVER_ERROR) + + tags = models.Tags( + tags=list( + map( + lambda row: models.Tag(id=row["id"], name=row["name"]), + rows, + ) + ) + ) + conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return {"tags": tags} + return asdict(tags), OK # 配信者のテーマ取得API @app.route("/api/user//theme", methods=["GET"]) -def get_streamer_theme_handler(username: str): +def get_streamer_theme_handler(username: str) -> tuple[dict[str, Any], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT id FROM users WHERE name = %s" c.execute(sql, [username]) - user = c.fetchone() - if user is None: - raise HttpException("not found", requests.codes["not_found"]) + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) sql = "SELECT * FROM themes WHERE user_id = %s" - c.execute(sql, [user["id"]]) - theme = c.fetchone() - if theme is None: - raise HttpException("not found", requests.codes["not_found"]) + c.execute(sql, [row["id"]]) + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) + + theme = models.Theme(id=row["id"], dark_mode=row["dark_mode"] == 1) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return {"id": theme["id"], "dark_mode": theme["dark_mode"] == 1} + return asdict(theme), OK # livestream # reserve livestream @app.route("/api/livestream/reservation", methods=["POST"]) -def reserve_livestream_handler(): +def reserve_livestream_handler() -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) req = get_request_json() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: # 2024/04/01からの1年間の期間内であるかチェック term_start_at = datetime(2024, 4, 1, 0, 0, 0, tzinfo=timezone.utc) term_end_at = datetime(2025, 4, 1, 0, 0, 0, tzinfo=timezone.utc) @@ -115,9 +151,7 @@ def reserve_livestream_handler(): ) if reserve_start_at >= term_end_at or reserve_end_at <= term_start_at: - raise HttpException( - "bad reservation time range", requests.codes["bad_request"] - ) + raise HttpException("bad reservation time range", BAD_REQUEST) # 予約枠をみて、予約が可能か調べる sql = ( @@ -129,7 +163,7 @@ def reserve_livestream_handler(): app.logger.error("予約枠一覧取得でエラー発生") raise HttpException( "failed to get reservation_slots", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) for slot in slots: slot_start_at = int(slot["start_at"]) @@ -141,7 +175,7 @@ def reserve_livestream_handler(): if not count: raise HttpException( "failed to get reservation_slots", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) count = count["slot"] @@ -149,18 +183,19 @@ def reserve_livestream_handler(): if count < 1: raise HttpException( f"予約区間 {req['start_at']} ~ {req['end_at']}が予約できません", - requests.codes["bad_request"], + BAD_REQUEST, ) - livestream_model = { - "user_id": sess_user_id, - "title": req["title"], - "description": req["description"], - "playlist_url": req["playlist_url"], - "thumbnail_url": req["thumbnail_url"], - "start_at": int(req["start_at"]), - "end_at": int(req["end_at"]), - } + livestream_model = models.LiveStreamModel( + id=0, # 未設定 + user_id=sess_user_id, + title=req["title"], + description=req["description"], + playlist_url=req["playlist_url"], + thumbnail_url=req["thumbnail_url"], + start_at=int(req["start_at"]), + end_at=int(req["end_at"]), + ) sql = "UPDATE reservation_slots SET slot = slot - 1 WHERE start_at >= %s AND end_at <= %s" c.execute(sql, [int(req["start_at"]), int(req["end_at"])]) @@ -169,51 +204,48 @@ def reserve_livestream_handler(): c.execute( sql, [ - sess_user_id, - req["title"], - req["description"], - req["playlist_url"], - req["thumbnail_url"], - int(req["start_at"]), - int(req["end_at"]), + livestream_model.user_id, + livestream_model.title, + livestream_model.description, + livestream_model.playlist_url, + livestream_model.thumbnail_url, + livestream_model.start_at, + livestream_model.end_at, ], ) - livestream_id = c.lastrowid - livestream_model["id"] = livestream_id + livestream_model.id = c.lastrowid # タグ追加 if "tags" in req and req["tags"]: for tag_id in req["tags"]: sql = "INSERT INTO livestream_tags (livestream_id, tag_id) VALUES (%s, %s)" - c.execute(sql, [livestream_id, tag_id]) + c.execute(sql, [livestream_model.id, tag_id]) livestream = fill_livestream_response(c, livestream_model) if not livestream: - raise HttpException( - "failed to fill livestream", requests.codes["internal_server_error"] - ) + raise HttpException("failed to fill livestream", INTERNAL_SERVER_ERROR) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livestream, requests.codes["created"] + return asdict(livestream), CREATED # list livestream @app.route("/api/livestream/search", methods=["GET"]) -def search_livestreams_handler(): +def search_livestreams_handler() -> tuple[list[dict[str, Any]], int]: key_tag_name = flask.request.args.get("tag") limit_str = flask.request.args.get("limit") try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: livestream_models = [] if key_tag_name: # タグによる取得 @@ -221,9 +253,7 @@ def search_livestreams_handler(): c.execute(sql, [key_tag_name]) tags = c.fetchall() if not tags: - raise HttpException( - "tag not match", requests.codes["internal_server_error"] - ) + raise HttpException("tag not match", INTERNAL_SERVER_ERROR) tag_id_list = list(map(lambda x: x["id"], tags)) sql = "SELECT * FROM livestream_tags WHERE tag_id IN (%s)" # idかtag_idか要確認 @@ -235,7 +265,17 @@ def search_livestreams_handler(): for key_tagged_livestream in key_tagged_livestreams: sql = "SELECT * FROM livestreams WHERE id = %s" c.execute(sql, key_tagged_livestream["livestream_id"]) - livestream_model = c.fetchone() + row = c.fetchone() + livestream_model = models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ) livestream_models.append(livestream_model) @@ -248,141 +288,206 @@ def search_livestreams_handler(): args.append(int(limit_str)) c.execute(sql, args) - livestream_models = c.fetchall() + rows = c.fetchall() + livestream_models = list( + map( + lambda row: models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ), + rows, + ) + ) livestreams = [] for livestream_model in livestream_models: livestream = fill_livestream_response(c, livestream_model) if not livestream: - raise HttpException( - "error", requests.codes["internal_server_error"] - ) - livestreams.append(livestream) + raise HttpException("error", INTERNAL_SERVER_ERROR) + + # HTTPレスポンスに使うのでasdictしてからリストに突っ込む + livestreams.append(asdict(livestream)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livestreams + return livestreams, OK @app.route("/api/livestream", methods=["GET"]) -def get_my_livestreams_handler(): +def get_my_livestreams_handler() -> tuple[list[dict[str, Any]], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM livestreams WHERE user_id = %s" c.execute(sql, [sess_user_id]) - livestream_models = c.fetchall() - if not livestream_models: + rows = c.fetchall() + if rows is None: raise HttpException( "failed to get livestreams", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) + if len(rows) == 0: + rows = [] + livestream_models = list( + map( + lambda row: models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ), + rows, + ) + ) livestreams = [] - for i, livestream_model in enumerate(livestream_models): + for livestream_model in livestream_models: livestream = fill_livestream_response(c, livestream_model) if not livestream: raise HttpException( "failed to fill livestream", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) - livestreams.append(livestream) + livestreams.append(asdict(livestream)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livestreams + return livestreams, OK @app.route("/api/user//livestream", methods=["GET"]) -def get_user_livestreams_handler(username): +def get_user_livestreams_handler(username) -> tuple[list[dict[str, Any]], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM users WHERE name = %s" c.execute(sql, [username]) - user = c.fetchone() - if not user: - raise HttpException("failed to get user", requests.codes["not_found"]) + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) + user = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) sql = "SELECT * FROM livestreams WHERE user_id = %s" - c.execute(sql, [user["id"]]) - livestream_models = c.fetchall() - if not livestream_models: + c.execute(sql, [user.id]) + rows = c.fetchall() + if rows is None: raise HttpException( "failed to get livestreams", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) + livestream_models = list( + map( + lambda row: models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ), + rows, + ) + ) + livestreams = [] - for i, livestream_model in enumerate(livestream_models): + for livestream_model in livestream_models: livestream = fill_livestream_response(c, livestream_model) if not livestream: raise HttpException( "failed to fill livestream", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) - livestreams.append(livestream) + livestreams.append(asdict(livestream)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livestreams + return livestreams, OK # get livestream @app.route("/api/livestream/", methods=["GET"]) -def get_livestream_handler(livestream_id: int): +def get_livestream_handler(livestream_id: int) -> tuple[dict[str, Any], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM livestreams WHERE id = %s" c.execute(sql, [livestream_id]) - livestream_model = c.fetchone() - if livestream_model is None: - raise HttpException("not found", requests.codes["not_found"]) + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) + livestream_model = models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ) livestream = fill_livestream_response(c, livestream_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livestream + return asdict(livestream), OK # get polling livecomment timeline @app.route("/api/livestream//livecomment", methods=["GET"]) -def get_livecomments_handler(livestream_id: int): +def get_livecomments_handler(livestream_id: int) -> tuple[list[dict[str, Any]], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM livecomments WHERE livestream_id = %s ORDER BY created_at DESC" args = [livestream_id] limit_str = flask.request.args.get("limit") @@ -391,52 +496,60 @@ def get_livecomments_handler(livestream_id: int): args.append(int(limit_str)) c.execute(sql, args) - livecomments = c.fetchall() - if livecomments is None: - raise HttpException("not found", requests.codes["not_found"]) - - if len(livecomments) == 0: - livecomments = [] + rows = c.fetchall() + if rows is None: + raise HttpException("not found", NOT_FOUND) + if len(rows) == 0: + rows = [] + + livecomment_models = list( + map( + lambda row: models.LiveCommentModel( + id=row["id"], + user_id=row["user_id"], + livestream_id=row["livestream_id"], + comment=row["comment"], + tip=row["tip"], + created_at=row["created_at"], + ), + rows, + ) + ) - for i, livecomment in enumerate(livecomments): - livecomment = fill_livecomment_response(c, livecomment) - if not livecomment: - raise HttpException( - "error", requests.codes["internal_server_error"] - ) - livecomments[i] = livecomment + livecomments: list[dict[str, Any]] = [] + for livecomment_model in livecomment_models: + livecomment = fill_livecomment_response(c, livecomment_model) + livecomments.append(asdict(livecomment)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livecomments + return livecomments, OK # ライブコメント投稿 @app.route("/api/livestream//livecomment", methods=["POST"]) -def post_livecomment_handler(livestream_id: int): +def post_livecomment_handler(livestream_id: int) -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) req = get_request_json() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT id, user_id, livestream_id, word FROM ng_words" c.execute(sql) ng_words = c.fetchall() if ng_words is None: - raise HttpException( - "failed to get NG words", requests.codes["internal_server_error"] - ) + raise HttpException("failed to get NG words", INTERNAL_SERVER_ERROR) for ng_word in ng_words: sql = """ @@ -450,15 +563,11 @@ def post_livecomment_handler(livestream_id: int): c.execute(sql, [req["comment"], ng_word["word"]]) hit_spam = c.fetchone() if not hit_spam: - raise HttpException( - "failed to get hitspam", requests.codes["internal_server_error"] - ) + raise HttpException("failed to get hitspam", INTERNAL_SERVER_ERROR) hit_spam = hit_spam["COUNT(*)"] app.logger.info(f"[hitSpam={hit_spam}] comment = {req['comment']}") if hit_spam >= 1: - raise HttpException( - "このコメントがスパム判定されました", requests.codes["bad_request"] - ) + raise HttpException("このコメントがスパム判定されました", BAD_REQUEST) now = int(datetime.now().timestamp()) @@ -468,64 +577,66 @@ def post_livecomment_handler(livestream_id: int): ) livecomment_id = c.lastrowid - livecomment_model = { - "id": livecomment_id, - "user_id": sess_user_id, - "livestream_id": livestream_id, - "comment": req["comment"], - "tip": req["tip"], - "created_at": now, - } + livecomment_model = models.LiveCommentModel( + id=livecomment_id, + user_id=sess_user_id, + livestream_id=livestream_id, + comment=req["comment"], + tip=req["tip"], + created_at=now, + ) livecomment = fill_livecomment_response(c, livecomment_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return livecomment, requests.codes["created"] + return asdict(livecomment), CREATED @app.route("/api/livestream//reaction", methods=["POST"]) -def post_reaction_handler(livestream_id: int): +def post_reaction_handler(livestream_id: int) -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) req = get_request_json() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: now = int(datetime.now().timestamp()) sql = "INSERT INTO reactions (user_id, livestream_id, emoji_name, created_at) VALUES (%s, %s, %s, %s)" c.execute(sql, [sess_user_id, livestream_id, req["emoji_name"], now]) reaction_id = c.lastrowid - reaction_model = { - "id": reaction_id, - "user_id": sess_user_id, - "livestream_id": livestream_id, - "emoji_name": req["emoji_name"], - "created_at": now, - } + reaction_model = models.ReactionModel( + id=reaction_id, + user_id=sess_user_id, + livestream_id=livestream_id, + emoji_name=req["emoji_name"], + created_at=now, + ) reaction = fill_reaction_response(c, reaction_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return reaction, requests.codes["created"] + return asdict(reaction), CREATED @app.route("/api/livestream//reaction", methods=["GET"]) -def get_reactions_handler(livestream_id: int): +def get_reactions_handler( + livestream_id: int, +) -> tuple[list[dict[str, Any]] | dict[str, Any], int]: verify_user_session() limit_str = flask.request.args.get("limit") @@ -533,7 +644,7 @@ def get_reactions_handler(livestream_id: int): try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM reactions WHERE livestream_id = %s ORDER BY created_at DESC" args = [livestream_id] if limit_str: @@ -541,53 +652,73 @@ def get_reactions_handler(livestream_id: int): args.append(int(limit_str)) c.execute(sql, args) - reaction_models = c.fetchall() - if reaction_models is None: + rows = c.fetchall() + if rows is None: app.logger.info("reaction_models") - raise HttpException( - "failed to get reactions", requests.codes["internal_server_error"] + raise HttpException("failed to get reactions", INTERNAL_SERVER_ERROR) + reaction_models = list( + map( + lambda row: models.ReactionModel( + id=row["id"], + user_id=row["user_id"], + livestream_id=row["livestream_id"], + emoji_name=row["emoji_name"], + created_at=row["created_at"], + ), + rows, ) + ) reactions = [] for reaction_model in reaction_models: reaction = fill_reaction_response(c, reaction_model) - reactions.append(reaction) + reactions.append(asdict(reaction)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return reactions + return reactions, OK # (配信者向け)ライブコメントの報告一覧取得API @app.route("/api/livestream//report", methods=["GET"]) -def get_livecomment_reports_handler(livestream_id: int): +def get_livecomment_reports_handler( + livestream_id: int, +) -> tuple[list[dict[str, Any]], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM livestreams WHERE id = %s" c.execute(sql, [livestream_id]) - livestream_model = c.fetchone() - if not livestream_model: + row = c.fetchone() + if not row: app.logger.info("livestream_model") - raise HttpException( - "failed to get livestream", requests.codes["internal_server_error"] - ) + raise HttpException("failed to get livestream", INTERNAL_SERVER_ERROR) + livestream_model = models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ) - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) - if livestream_model["user_id"] != sess_user_id: + if livestream_model.user_id != sess_user_id: raise HttpException( "can't get other streamer's livecomment reports", - requests.codes["forbidden"], + FORBIDDEN, ) sql = "SELECT * FROM livecomment_reports WHERE livestream_id = %s" @@ -597,7 +728,7 @@ def get_livecomment_reports_handler(livestream_id: int): app.logger.info("report_model") raise HttpException( "failed to get livecomment reports", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) reports = [] @@ -607,44 +738,44 @@ def get_livecomment_reports_handler(livestream_id: int): app.logger.info("failed to fill livecomment report") raise HttpException( "failed to fill livecomment report", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) - reports.append(report) + reports.append(asdict(report)) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return reports + return reports, OK @app.route("/api/livestream//ngwords", methods=["GET"]) def get_ngwords(livestream_id: int): verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM ng_words WHERE user_id = %s AND livestream_id = %s ORDER BY created_at DESC" c.execute(sql, [sess_user_id, livestream_id]) ngwords = c.fetchall() if ngwords is None: app.logger.error("failed to get ngwords") - raise HttpException("error", requests.codes["internal_server_error"]) + raise HttpException("error", INTERNAL_SERVER_ERROR) if len(ngwords) == 0: return [] conn.commit() return ngwords except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() @@ -654,65 +785,64 @@ def get_ngwords(livestream_id: int): "/api/livestream//livecomment//report", methods=["POST"], ) -def report_livecomment_handler(livestream_id: int, livecomment_id: int): +def report_livecomment_handler( + livestream_id: int, livecomment_id: int +) -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException( - "failed to find user-id from session", requests.codes["unauthorized"] - ) + raise HttpException("failed to find user-id from session", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: created_at = int(datetime.now().timestamp()) - report_model = { - "user_id": sess_user_id, - "livestream_id": livestream_id, - "livecomment_id": livecomment_id, - "created_at": created_at, - } sql = "INSERT INTO livecomment_reports (user_id, livestream_id, livecomment_id, created_at) VALUES (%s, %s, %s, %s)" c.execute(sql, [sess_user_id, livestream_id, livecomment_id, created_at]) report_id = c.lastrowid - report_model["id"] = report_id + + report_model = models.LiveCommentReportModel( + id=report_id, + user_id=sess_user_id, + livestream_id=livestream_id, + livecomment_id=livecomment_id, + created_at=created_at, + ) report = fill_livecomment_report_response(c, report_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return report, requests.codes["created"] + return asdict(report), CREATED # 配信者によるモデレーション (NGワード登録) @app.route("/api/livestream//moderate", methods=["POST"]) -def moderate_handler(livestream_id: int): +def moderate_handler(livestream_id: int) -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException( - "failed to find user-id from session", requests.codes["unauthorized"] - ) + raise HttpException("failed to find user-id from session", UNAUTHORIZED) req = get_request_json() if not req or "ng_word" not in req: raise HttpException( "failed to decode the request body as json", - requests.codes["bad_request"], + BAD_REQUEST, ) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: # 配信者自身の配信に対するmoderateなのかを検証 sql = "SELECT * FROM livestreams WHERE id = %s AND user_id = %s" c.execute(sql, [livestream_id, sess_user_id]) @@ -720,7 +850,7 @@ def moderate_handler(livestream_id: int): if owned_livestreams is None or len(owned_livestreams) == 0: raise HttpException( "A streamer can't moderate livestreams that other streamers own", - requests.codes["bad_request"], + BAD_REQUEST, ) sql = "INSERT INTO ng_words(user_id, livestream_id, word, created_at) VALUES (%s, %s, %s, %s)" @@ -735,6 +865,7 @@ def moderate_handler(livestream_id: int): ) word_id = c.lastrowid + app.logger.info(f"word_id: {word_id}, word: {req['ng_word']}") sql = "SELECT * FROM ng_words" c.execute(sql) @@ -749,10 +880,11 @@ def moderate_handler(livestream_id: int): app.logger.warn("failed to get livecomments") raise HttpException( "failed to get livecomments", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) for livecomment in livecomments: + app.logger.info(f"delete: {livecomment}") sql = """ DELETE FROM livecomments WHERE @@ -770,28 +902,26 @@ def moderate_handler(livestream_id: int): conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return {"word_id": word_id}, requests.codes["created"] + return {"word_id": word_id}, CREATED # livestream_viewersにINSERTするため必要 # ユーザ視聴開始 (viewer) @app.route("/api/livestream//enter", methods=["POST"]) -def enter_livestream_handler(livestream_id: int): +def enter_livestream_handler(livestream_id: int) -> tuple[str, int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException( - "failed to find user-id from session", requests.codes["unauthorized"] - ) + raise HttpException("failed to find user-id from session", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "INSERT INTO livestream_viewers_history (user_id, livestream_id, created_at) VALUES(%s, %s, %s)" c.execute( sql, [sess_user_id, livestream_id, int(datetime.now().timestamp())] @@ -799,48 +929,46 @@ def enter_livestream_handler(livestream_id: int): conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return "" + return "", OK # ユーザ視聴終了 (viewer) @app.route("/api/livestream//exit", methods=["DELETE"]) -def exit_livestream_handler(livestream_id: int): +def exit_livestream_handler(livestream_id: int) -> tuple[str, int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException( - "failed to find user-id from session", requests.codes["unauthorized"] - ) + raise HttpException("failed to find user-id from session", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "DELETE FROM livestream_viewers_history WHERE user_id = %s AND livestream_id = %s" c.execute(sql, [sess_user_id, livestream_id]) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return "" + return "", OK # user @app.route("/api/register", methods=["POST"]) -def register_handler(): +def register_handler() -> tuple[dict[str, Any], int]: req = get_request_json() if not req: raise HttpException( "failed to decode the request body as json", - requests.codes["bad_request"], + BAD_REQUEST, ) if not all( @@ -849,44 +977,43 @@ def register_handler(): ): raise HttpException( "failed to decode the request body as json", - requests.codes["bad_request"], + BAD_REQUEST, ) if req["name"] == "pipe": - raise HttpException( - "the username 'pipe' is reserved", requests.codes["bad_request"] - ) + raise HttpException("the username 'pipe' is reserved", BAD_REQUEST) hashed_password = bcrypt.hashpw(req["password"].encode(), bcrypt.gensalt(rounds=4)) - user_model = { - "name": req["name"], - "display_name": req["display_name"], - "description": req["description"], - "password": hashed_password, - } + user_model = models.UserModel( + id=0, # INSERT後に確定する + name=req["name"], + display_name=req["display_name"], + description=req["description"], + password=hashed_password.decode(), + ) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "INSERT INTO users (name, display_name, description, password) VALUES (%s,%s,%s,%s)" c.execute( sql, [ - user_model["name"], - user_model["display_name"], - user_model["description"], - user_model["password"], + user_model.name, + user_model.display_name, + user_model.description, + user_model.password, ], ) user_id = c.lastrowid - user_model["id"] = user_id + user_model.id = user_id sql = "INSERT INTO themes (user_id, dark_mode) VALUES (%s,%s)" c.execute(sql, [user_id, req["theme"]["dark_mode"]]) - if os.getenv("ISUCON13_POWERDNS_DISABLED") != "true": + if Settings.POWERDNS_ENABLED and Settings.POWERDNS_SUBDOMAIN_ADDRESS: app.logger.info("add-record") result = subprocess.run( [ @@ -896,63 +1023,62 @@ def register_handler(): req["name"], "A", "30", - os.getenv("ISUCON13_POWERDNS_SUBDOMAIN_ADDRESS"), + Settings.POWERDNS_SUBDOMAIN_ADDRESS, ], capture_output=True, text=True, ) if result.returncode != 0: - raise HttpException( - result.stdout, requests.codes["internal_server_error"] - ) + raise HttpException(result.stdout, INTERNAL_SERVER_ERROR) user = fill_user_response(c, user_model) conn.commit() except pymysql.err.IntegrityError: - raise HttpException( - "failed to insert user", requests.codes["internal_server_error"] - ) + raise HttpException("failed to insert user", INTERNAL_SERVER_ERROR) finally: conn.close() - return user, requests.codes["created"] + return asdict(user), CREATED @app.route("/api/login", methods=["POST"]) -def login_handler(): +def login_handler() -> tuple[str, int]: try: req = get_request_json() if not req: raise HttpException( "failed to decode the request body as json", - requests.codes["bad_request"], + BAD_REQUEST, ) conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM users WHERE name = %s" c.execute(sql, [req["username"]]) - user = c.fetchone() - if not user: - raise HttpException( - "invalid username or password", requests.codes["unauthorized"] - ) + row = c.fetchone() + if row is None: + raise HttpException("invalid username or password", UNAUTHORIZED) + user = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) - if not bcrypt.checkpw(req["password"].encode(), user["password"].encode()): - raise HttpException( - "invalid username or password", requests.codes["unauthorized"] - ) + if not bcrypt.checkpw(req["password"].encode(), user.password.encode()): + raise HttpException("invalid username or password", UNAUTHORIZED) session_end_at = datetime.now() + timedelta(hours=1) session_id = str(uuid.uuid4()) # sessions.Options周りはapp.configで設定済み - flask.session[Constants.DEFAULT_SESSION_ID_KEY] = session_id + flask.session[Settings.DEFAULT_SESSION_ID_KEY] = session_id # FIXME: ユーザ名 - flask.session[Constants.DEFAULT_USER_ID_KEY] = user["id"] - flask.session[Constants.DEFAULT_USER_NAME_KEY] = user["name"] - flask.session[Constants.DEFAULT_SESSION_EXPIRES_KEY] = int( + flask.session[Settings.DEFAULT_USER_ID_KEY] = user.id + flask.session[Settings.DEFAULT_USER_NAME_KEY] = user.name + flask.session[Settings.DEFAULT_SESSION_EXPIRES_KEY] = int( session_end_at.timestamp() ) @@ -960,61 +1086,80 @@ def login_handler(): except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return "", requests.codes["ok"] + return "", OK @app.route("/api/user/me", methods=["GET"]) -def get_me_handler(): +def get_me_handler() -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM users WHERE id = %s" c.execute(sql, [sess_user_id]) - user_model = c.fetchone() + row = c.fetchone() + if not row: + raise HttpException("failed to get user", INTERNAL_SERVER_ERROR) + user_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) user = fill_user_response(c, user_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return user + return asdict(user), OK # フロントエンドで、配信予約のコラボレーターを指定する際に必要 @app.route("/api/user/", methods=["GET"]) -def get_user_handler(username: str): +def get_user_handler(username: str) -> tuple[dict[str, Any], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM users WHERE name = %s" c.execute(sql, [username]) - user_model = c.fetchone() + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) + user_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) + user = fill_user_response(c, user_model) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return user + return asdict(user), OK @app.route("/api/user//statistics", methods=["GET"]) -def get_user_statistics_handler(username: str): +def get_user_statistics_handler(username: str) -> tuple[dict[str, Any], int]: verify_user_session() # ユーザごとに、紐づく配信について、累計リアクション数、累計ライブコメント数、累計売上金額を算出 @@ -1023,16 +1168,38 @@ def get_user_statistics_handler(username: str): try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: # ??? sql = "SELECT * FROM users WHERE name = %s" c.execute(sql, [username]) - user = c.fetchone() + + row = c.fetchone() + if row is None: + raise HttpException("not found", NOT_FOUND) + user = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) # ランク算出 sql = "SELECT * FROM users" c.execute(sql) - users = c.fetchall() + rows = c.fetchall() + users = list( + map( + lambda row: models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ), + rows, + ) + ) ranking = [] for user in users: @@ -1042,12 +1209,12 @@ def get_user_statistics_handler(username: str): INNER JOIN reactions r ON r.livestream_id = l.id WHERE u.id = %s """ - c.execute(sql, [user["id"]]) + c.execute(sql, [user.id]) reactions = c.fetchone() if not reactions: raise HttpException( "failed to count reactions", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) sql = """ @@ -1056,16 +1223,16 @@ def get_user_statistics_handler(username: str): INNER JOIN livecomments l2 ON l2.livestream_id = l.id WHERE u.id = %s """ - c.execute(sql, [user["id"]]) + c.execute(sql, [user.id]) tips = c.fetchone() if not tips: raise HttpException( "failed to count tips", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) score = reactions["COUNT(*)"] + tips["IFNULL(SUM(l2.tip), 0)"] - ranking.append({"username": user["name"], "score": score}) + ranking.append({"username": user.name, "score": score}) ranking = sorted(ranking, key=lambda x: x["score"]) rank = 1 @@ -1090,7 +1257,7 @@ def get_user_statistics_handler(username: str): if not total_reactions: raise HttpException( "failed to count total reactions", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) total_reactions = total_reactions["COUNT(*)"] @@ -1099,13 +1266,13 @@ def get_user_statistics_handler(username: str): total_tip = 0 for user in users: sql = "SELECT * FROM livestreams WHERE user_id = %s" - c.execute(sql, [user["id"]]) + c.execute(sql, [user.id]) livestreams = c.fetchall() if livestreams is None: app.logger.error("livestreams livecomments") raise HttpException( "failed to get livestreams", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) for livestream in livestreams: @@ -1116,7 +1283,7 @@ def get_user_statistics_handler(username: str): app.logger.error("livecomments") raise HttpException( "failed to get livecomments", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) for livecomment in livecomments: total_tip += livecomment["tip"] @@ -1126,13 +1293,13 @@ def get_user_statistics_handler(username: str): viewers_count = 0 for user in users: sql = "SELECT * FROM livestreams WHERE user_id = %s" - c.execute(sql, [user["id"]]) + c.execute(sql, [user.id]) livestreams = c.fetchall() if livestreams is None: app.logger.error("viewers_count") raise HttpException( "failed to get livestreams", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) for livestream in livestreams: @@ -1142,7 +1309,7 @@ def get_user_statistics_handler(username: str): if not cnt: raise HttpException( "failed to get livestream_view_history", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) viewers_count += cnt["COUNT(*)"] @@ -1164,48 +1331,56 @@ def get_user_statistics_handler(username: str): else: favorite_emoji = favorite_emoji["emoji_name"] + statistics = models.UserStatistics( + rank=rank, + viewers_count=viewers_count, + total_reactions=total_reactions, + total_livecomments=total_livecomments, + total_tip=total_tip, + favorite_emoji=favorite_emoji, + ) + conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return { - "rank": rank, - "viewers_count": viewers_count, - "total_reactions": total_reactions, - "total_livecomments": total_livecomments, - "total_tip": total_tip, - "favorite_emoji": favorite_emoji, - } + return asdict(statistics), OK @app.route("/api/user//icon", methods=["GET"]) -def get_icon_handler(username: str): +def get_icon_handler(username: str) -> flask.Response: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT * FROM users WHERE name = %s" c.execute(sql, [username]) - user = c.fetchone() - if not user: - raise HttpException( - "user not found", requests.codes["internal_server_error"] - ) + + row = c.fetchone() + if row is None: + raise HttpException("user not found", INTERNAL_SERVER_ERROR) + user = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) sql = "SELECT image FROM icons WHERE user_id = %s" - c.execute(sql, [user["id"]]) + c.execute(sql, [user.id]) image = c.fetchone() conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() @@ -1222,25 +1397,25 @@ def get_icon_handler(username: str): @app.route("/api/icon", methods=["POST"]) -def post_icon_handler(): +def post_icon_handler() -> tuple[dict[str, Any], int]: verify_user_session() - sess_user_id = flask.session.get(Constants.DEFAULT_USER_ID_KEY) + sess_user_id = flask.session.get(Settings.DEFAULT_USER_ID_KEY) if not sess_user_id: - raise HttpException("unauthorized", requests.codes["unauthorized"]) + raise HttpException("unauthorized", UNAUTHORIZED) req = get_request_json() if not req or "image" not in req: raise HttpException( "failed to decode the request body as json", - requests.codes["bad_request"], + BAD_REQUEST, ) new_icon = b64decode(req["image"]) try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "DELETE FROM icons WHERE user_id = %s" c.execute(sql, [sess_user_id]) @@ -1251,39 +1426,35 @@ def post_icon_handler(): conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return {"id": icon_id}, requests.codes["created"] + return {"id": icon_id}, CREATED # stats # ライブコメント統計情報 @app.route("/api/livestream//statistics", methods=["GET"]) -def get_livestream_statistics_handler(livestream_id: int): +def get_livestream_statistics_handler(livestream_id: int) -> tuple[dict[str, Any], int]: verify_user_session() try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: # ?? sql = "SELECT * FROM livestreams WHERE id = %s" c.execute(sql, [livestream_id]) livestream = c.fetchone() if livestream is None: - raise HttpException( - "failed to get livestream", requests.codes["internal_server_error"] - ) + raise HttpException("failed to get livestream", INTERNAL_SERVER_ERROR) sql = "SELECT * FROM livestreams" c.execute(sql) livestreams = c.fetchall() if livestreams is None: - raise HttpException( - "failed to get livestreams", requests.codes["internal_server_error"] - ) + raise HttpException("failed to get livestreams", INTERNAL_SERVER_ERROR) # ランク算出 ranking = [] @@ -1294,7 +1465,7 @@ def get_livestream_statistics_handler(livestream_id: int): if reactions is None: raise HttpException( "failed to get livestream", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) reactions = reactions["COUNT(*)"] @@ -1302,9 +1473,7 @@ def get_livestream_statistics_handler(livestream_id: int): c.execute(sql, [livestream_id]) total_tips = c.fetchone() if total_tips is None: - raise HttpException( - "failed to count tips", requests.codes["internal_server_error"] - ) + raise HttpException("failed to count tips", INTERNAL_SERVER_ERROR) total_tips = total_tips["IFNULL(SUM(l2.tip), 0)"] score = reactions + total_tips @@ -1333,7 +1502,7 @@ def get_livestream_statistics_handler(livestream_id: int): if viewers_count is None: raise HttpException( "failed to get viewers_count", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) viewers_count = viewers_count["COUNT(*)"] @@ -1344,7 +1513,7 @@ def get_livestream_statistics_handler(livestream_id: int): if max_tip is None: raise HttpException( "failed to get max_tip", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) max_tip = max_tip["IFNULL(MAX(tip), 0)"] @@ -1355,7 +1524,7 @@ def get_livestream_statistics_handler(livestream_id: int): if total_reactions is None: raise HttpException( "failed to get total_reactions", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) total_reactions = total_reactions["COUNT(*)"] @@ -1366,21 +1535,23 @@ def get_livestream_statistics_handler(livestream_id: int): if total_reports is None: raise HttpException( "failed to get total_reports", - requests.codes["internal_server_error"], + INTERNAL_SERVER_ERROR, ) total_reports = total_reports["COUNT(*)"] + + user_statistics = models.LiveStreamStatistics( + rank=rank, + viewers_count=viewers_count, + total_reactions=total_reactions, + total_reports=total_reports, + max_tip=max_tip, + ) except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() - return { - "rank": rank, - "viewers_count": viewers_count, - "max_tip": max_tip, - "total_reactions": total_reactions, - "total_reports": total_reports, - } + return asdict(user_statistics), OK # 課金情報 @@ -1389,18 +1560,16 @@ def get_payment_result(): try: conn = dbh() conn.begin() - with conn.cursor() as c: + with conn.cursor(pymysql.cursors.DictCursor) as c: sql = "SELECT IFNULL(SUM(tip), 0) FROM livecomments" c.execute(sql) total_tip = c.fetchone() if not total_tip: - raise HttpException( - "failed to count total tip", requests.codes["internal_server_error"] - ) + raise HttpException("failed to count total tip", INTERNAL_SERVER_ERROR) conn.commit() except pymysql.Error as err: app.logger.exception(err) - raise HttpException("db error", requests.codes["internal_server_error"]) + raise HttpException("db error", INTERNAL_SERVER_ERROR) finally: conn.close() @@ -1408,155 +1577,229 @@ def get_payment_result(): def verify_user_session(): - sess = flask.session.get(Constants.DEFAULT_SESSION_ID_KEY) + sess = flask.session.get(Settings.DEFAULT_SESSION_ID_KEY) if not sess: - raise HttpException("invalid session", requests.codes["internal_server_error"]) + raise HttpException("invalid session", INTERNAL_SERVER_ERROR) - session_expires = flask.session.get(Constants.DEFAULT_SESSION_EXPIRES_KEY) + session_expires = flask.session.get(Settings.DEFAULT_SESSION_EXPIRES_KEY) if not session_expires: - raise HttpException("forbidden", requests.codes["forbidden"]) + raise HttpException("forbidden", FORBIDDEN) now = datetime.now() if int(now.timestamp()) > session_expires: - raise HttpException("session has expired", requests.codes["unauthorized"]) + raise HttpException("session has expired", UNAUTHORIZED) return def fill_livecomment_response( - c: pymysql.cursors.Cursor, livecomment_model -) -> None | dict: + c: pymysql.cursors.DictCursor, livecomment_model: models.LiveCommentModel +) -> models.LiveComment: sql = "SELECT * FROM users WHERE id = %s" - c.execute(sql, [livecomment_model["user_id"]]) - comment_owner_model = c.fetchone() - if not comment_owner_model: + c.execute(sql, [livecomment_model.user_id]) + row = c.fetchone() + if not row: app.logger.error("failed to get comment_owner_model") - return None + raise HttpException("failed to get comment_owner_model", INTERNAL_SERVER_ERROR) + comment_owner_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) comment_owner = fill_user_response(c, comment_owner_model) sql = "SELECT * FROM livestreams WHERE id = %s" - c.execute(sql, [livecomment_model["livestream_id"]]) - livestream_model = c.fetchone() - if not livestream_model: + c.execute(sql, [livecomment_model.livestream_id]) + row = c.fetchone() + if not row: app.logger.error("failed to get livestream_model") - return None + raise HttpException("failed to get livestream_model", INTERNAL_SERVER_ERROR) + + livestream_model = models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ) livestream = fill_livestream_response(c, livestream_model) - return { - "id": livecomment_model["id"], - "user": comment_owner, - "livestream": livestream, - "comment": livecomment_model["comment"], - "tip": livecomment_model["tip"], - "created_at": livecomment_model["created_at"], - } + return models.LiveComment( + id=livecomment_model.id, + user=comment_owner, + livestream=livestream, + comment=livecomment_model.comment, + tip=livecomment_model.tip, + created_at=livecomment_model.created_at, + ) -def fill_reaction_response(c: pymysql.cursors.Cursor, reaction_model) -> None | dict: +def fill_reaction_response( + c: pymysql.cursors.DictCursor, reaction_model: models.ReactionModel +) -> models.Reaction: sql = "SELECT * FROM users WHERE id = %s" - c.execute(sql, [reaction_model["user_id"]]) - user_model = c.fetchone() - if not user_model: + c.execute(sql, [reaction_model.user_id]) + row = c.fetchone() + if not row: app.logger.error("failed to get user_model") - return None + raise HttpException("failed to get user_model", INTERNAL_SERVER_ERROR) + user_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) user = fill_user_response(c, user_model) sql = "SELECT * FROM livestreams WHERE id = %s" - c.execute(sql, [reaction_model["livestream_id"]]) - livestream_model = c.fetchone() - if not livestream_model: - app.logger.error("failed to get livestream_model") - return None + c.execute(sql, [reaction_model.livestream_id]) + row = c.fetchone() + if not row: + app.logger.error("failed to get livestream_model") + raise HttpException("livestream_model", INTERNAL_SERVER_ERROR) + livestream_model = models.LiveStreamModel( + id=row["id"], + user_id=row["user_id"], + title=row["title"], + description=row["description"], + playlist_url=row["playlist_url"], + thumbnail_url=row["thumbnail_url"], + start_at=int(row["start_at"]), + end_at=int(row["end_at"]), + ) livestream = fill_livestream_response(c, livestream_model) - return { - "id": reaction_model["id"], - "user": user, - "livestream": livestream, - "emoji_name": reaction_model["emoji_name"], - "created_at": reaction_model["created_at"], - } + return models.Reaction( + id=reaction_model.id, + user=user, + livestream=livestream, + emoji_name=reaction_model.emoji_name, + created_at=reaction_model.created_at, + ) def fill_livecomment_report_response( - c: pymysql.cursors.Cursor, report_model -) -> None | dict: + c: pymysql.cursors.DictCursor, report_model: models.LiveCommentReportModel +) -> models.LiveCommentReport: sql = "SELECT * FROM users WHERE id = %s" - c.execute(sql, [report_model["user_id"]]) - reporter_model = c.fetchone() + c.execute(sql, [report_model.user_id]) + row = c.fetchone() + if not row: + raise HttpException("failed to get reporter user", INTERNAL_SERVER_ERROR) + reporter_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) reporter = fill_user_response(c, reporter_model) sql = "SELECT * FROM livecomments WHERE id = %s" - c.execute(sql, [report_model["livecomment_id"]]) - livecomment_model = c.fetchone() + c.execute(sql, [report_model.livecomment_id]) + row = c.fetchone() + if row is None: + raise HttpException("failed to get livecomment", INTERNAL_SERVER_ERROR) + livecomment_model = models.LiveCommentModel( + id=row["id"], + user_id=row["user_id"], + livestream_id=row["livestream_id"], + comment=row["comment"], + tip=row["tip"], + created_at=row["created_at"], + ) livecomment = fill_livecomment_response(c, livecomment_model) - return { - "id": report_model["id"], - "reporter": reporter, - "livecomment": livecomment, - "created_at": report_model["created_at"], - } + return models.LiveCommentReport( + id=report_model.id, + reporter=reporter, + livecomment=livecomment, + created_at=report_model.created_at, + ) def fill_livestream_response( - c: pymysql.cursors.Cursor, livestream_model -) -> None | dict: + c: pymysql.cursors.DictCursor, livestream_model: models.LiveStreamModel +) -> models.LiveStream: sql = "SELECT * FROM users WHERE id = %s" - c.execute(sql, [livestream_model["user_id"]]) - owner_model = c.fetchone() - if not owner_model: - return None + c.execute(sql, [livestream_model.user_id]) + row = c.fetchone() + if not row: + raise HttpException("failed to get owner_model", INTERNAL_SERVER_ERROR) + owner_model = models.UserModel( + id=row["id"], + name=row["name"], + display_name=row["display_name"], + description=row["description"], + password=row["password"], + ) owner = fill_user_response(c, owner_model) sql = "SELECT * FROM livestream_tags WHERE livestream_id = %s" - c.execute(sql, [livestream_model["id"]]) - livestream_tags = c.fetchall() + c.execute(sql, [livestream_model.id]) + rows = c.fetchall() tags = [] - for livestream_tag in livestream_tags: + for row in rows: + livestream_tag = models.LiveStreamTagModel( + id=row["id"], + livestream_id=row["livestream_id"], + tag_id=row["tag_id"], + ) sql = "SELECT * FROM tags WHERE id = %s" - c.execute(sql, [livestream_tag["tag_id"]]) - tags.append(c.fetchone()) - - livestream = { - "id": livestream_model["id"], - "owner": owner, - "title": livestream_model["title"], - "tags": tags, - "description": livestream_model["description"], - "playlist_url": livestream_model["playlist_url"], - "thumbnail_url": livestream_model["thumbnail_url"], - "start_at": livestream_model["start_at"], - "end_at": livestream_model["end_at"], - } + c.execute(sql, [livestream_tag.tag_id]) + tag_row = c.fetchone() + if not tag_row: + raise HttpException("failed to get tags", INTERNAL_SERVER_ERROR) + tag = models.Tag(id=row["id"], name=tag_row["name"]) + tags.append(tag) + + livestream = models.LiveStream( + id=livestream_model.id, + owner=owner, + title=livestream_model.title, + tags=tags, + description=livestream_model.description, + playlist_url=livestream_model.playlist_url, + thumbnail_url=livestream_model.thumbnail_url, + start_at=livestream_model.start_at, + end_at=livestream_model.end_at, + ) return livestream -def fill_user_response(c: pymysql.cursors.Cursor, user_model): +def fill_user_response( + c: pymysql.cursors.DictCursor, user_model: models.UserModel +) -> models.User: sql = "SELECT * FROM themes WHERE user_id = %s" - c.execute(sql, [user_model["id"]]) + c.execute(sql, [user_model.id]) theme = c.fetchone() if theme is None: - return "not found", requests.codes["not_found"] - - user = { - "id": user_model["id"], - "name": user_model["name"], - "display_name": user_model["display_name"], - "description": user_model["description"], - "theme": { - "id": theme["id"], - "dark_mode": theme["dark_mode"] == 1, - }, - } + raise HttpException("not found", NOT_FOUND) + + user = models.User( + id=user_model.id, + name=user_model.name, + display_name=user_model.display_name, + description=user_model.description, + theme=models.Theme( + id=theme["id"], + dark_mode=theme["dark_mode"] == 1, + ), + ) return user @@ -1566,19 +1809,15 @@ def dbh(): return flask.g.db flask.g.db = pymysql.connect( - host=os.getenv("ISUCON13_MYSQL_DIALCONFIG_ADDRESS", "127.0.0.1"), - port=int(os.getenv("ISUCON13_MYSQL_DIALCONFIG_PORT", 3306)), - user=os.getenv("ISUCON13_MYSQL_DIALCONFIG_USER", "isucon"), - password=os.getenv("ISUCON13_MYSQL_DIALCONFIG_PASSWORD", "isucon"), - db=os.getenv("ISUCON13_MYSQL_DIALCONFIG_DATABASE", "isupipe"), + host=Settings.DB_HOST, + port=Settings.DB_PORT, + user=Settings.DB_USER, + password=Settings.DB_PASSWORD, + db=Settings.DB_NAME, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, autocommit=True, ) - # cur = flask.g.db.cursor() - # cur.execute( - # "SET SESSION sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'" - # ) return flask.g.db @@ -1609,20 +1848,8 @@ def handle_http_exception(error): if __name__ == "__main__": - # // DB接続 - # conn, err := connectDB(e.Logger) - # if err != nil { - # e.Logger.Errorf("failed to connect db: %v", err) - # os.Exit(1) - # } - # defer conn.Close() - # dbConn = conn - - # subdomainAddr, ok := os.LookupEnv(powerDNSSubdomainAddressEnvKey) - # if !ok { - # e.Logger.Errorf("environ %s must be provided", powerDNSSubdomainAddressEnvKey) - # os.Exit(1) - # } - # powerDNSSubdomainAddress = subdomainAddr - + app.secret_key = Settings.APP_SECRET_KEY + app.config["SESSION_COOKIE_DOMAIN"] = Settings.SESSION_COOKIE_DOMAIN + app.config["SESSION_COOKIE_PATH"] = Settings.SESSION_COOKIE_PATH + app.config["PERMANENT_SESSION_LIFETIME"] = Settings.PERMANENT_SESSION_LIFETIME app.run(host="0.0.0.0", port=8080, debug=True, threaded=True) diff --git a/webapp/python/models.py b/webapp/python/models.py new file mode 100644 index 000000000..3b99dfffa --- /dev/null +++ b/webapp/python/models.py @@ -0,0 +1,162 @@ +from dataclasses import dataclass + + +@dataclass +class UserModel: + id: int + name: str + display_name: str + description: str + password: str # hashed + + +@dataclass +class Theme: + id: int + dark_mode: bool + + +@dataclass +class ThemeModel: + id: int + user_id: int + dark_mode: bool + + +@dataclass +class User: + id: int + name: str + display_name: str + description: str + theme: Theme + + +@dataclass +class Tag: + id: int + name: str + + +@dataclass +class Tags: + tags: list[Tag] + + +@dataclass +class LiveStreamModel: + id: int + user_id: int + title: str + description: str + playlist_url: str + thumbnail_url: str + start_at: int + end_at: int + + +@dataclass +class LiveStream: + id: int + owner: User + title: str + description: str + playlist_url: str + thumbnail_url: str + tags: list[Tag] + start_at: int + end_at: int + + +@dataclass +class LiveStreamTagModel: + id: int + livestream_id: int + tag_id: int + + +@dataclass +class LiveCommentModel: + id: int + user_id: int + livestream_id: int + comment: str + tip: int + created_at: int + + +@dataclass +class LiveComment: + id: int + user: User + livestream: LiveStream + comment: str + tip: int + created_at: int + + +@dataclass +class ReactionModel: + id: int + emoji_name: str + user_id: int + livestream_id: int + created_at: int + + +@dataclass +class Reaction: + id: int + emoji_name: str + user: User + livestream: LiveStream + created_at: int + + +@dataclass +class LiveCommentReportModel: + id: int + user_id: int + livestream_id: int + livecomment_id: int + created_at: int + + +@dataclass +class LiveCommentReport: + id: int + reporter: User + livecomment: LiveComment + created_at: int + + +@dataclass +class LiveStreamStatistics: + rank: int + viewers_count: int + total_reactions: int + total_reports: int + max_tip: int + + +@dataclass +class LiveStreamRankingEntry: + livestream_id: int + title: str + score: int + + +@dataclass +class UserStatistics: + rank: int + viewers_count: int + total_reactions: int + total_livecomments: int + total_tip: int + favorite_emoji: str + + +@dataclass +class UserRankingEntry: + username: str + score: int