From 80da311288ef6f2a4d727a33a8c607cc58cb5026 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 16:13:41 +0300 Subject: [PATCH] FIX: ip detction and ip hashing --- chat.db | Bin 24576 -> 32768 bytes controllers/__pycache__/chat.cpython-314.pyc | Bin 14244 -> 14612 bytes controllers/chat.py | 33 ++++++++++++++----- index.html | 25 +++++++++++--- models/__pycache__/base.cpython-314.pyc | Bin 5122 -> 5138 bytes models/__pycache__/db.cpython-314.pyc | Bin 16478 -> 17643 bytes models/base.py | 4 +-- models/db.py | 24 ++++++++++++-- 8 files changed, 68 insertions(+), 18 deletions(-) diff --git a/chat.db b/chat.db index 23b1d026f12bb7ffd7ddbfe475d40c68275791fd..74f9db341c5fc47f0cdc7c9ebfb3dfe665471c46 100644 GIT binary patch delta 4055 zcma)9O^oB_d7c^X{Ay;$Yq)h63vV|`(XJaa=ATF@MOu?0C6b~jk)kL{1xk<<|NkY5 zlmry=r0^j*#ho0QOA4guEl6OjfgWO@2HZn$0RkjwPi+bm=&3*Gq1SRov%B7=C@SJR zke~2A@AL3^--3{{kw*{rzV_`mFHZh`@9>!4 zJ2Ni+{o>-7Kfk`YKEC?d(b=Dz9UZ^*r7!J&s9kHdhx^CZ>)OwMQ%lm^OX4RZffYye zlc#AChMG5f_sQ=0^&g%u+1hCTTW`Jd&i;p;TsK1N_Af<}TMI1-XRqEJwj`R6=_fKl z^7NB0zq&zuo`+XOr63_YaWc>z|#&mnWBx_vnhr-tQeAz14r` z`WNS4d9#YHL9d25dUkQ~UkAT_dRE`N)bIcN;x8|rod0R7c*T#XL*_8lv>=6Y!B4cjmv7}X8KGO@Zt-2BLF!uMo$HeFyrhU#t0nc5mUwIaIN z>WAGK>j#!a*40E9X{oF(_;gB0l7>)WY1%Bq_u1hD_t%UFH~YP)lY1ZuZWJ?U(Rdg6|0q7SgV=0P}FBH>!x zr8E2X)#KyaZ2tIg?|C-=hd8aYAT*URok^}v=ZpC`w38AZdz;yKlTcC!a0ci`31UsP zP#d-5(r4W6CM*=PF&eZ7+Q=12mx}Ph(E}CgXv)G@v^isxQke`o127dgsMv=Z;gB4# zw}Q2P=j!p%iwv(ne*4Lm=P{kEMl zGj`N)#Z0qw#RpNtkVd3zB%}-yDL|UxbQpqzI+jeqVLmW945E5%Z^F1)Rl>Wqobqo9EZ3sNU!UoFa+K9#r*fG#`XU@;%@+Hm6u#3&&_6c^>L;F*n% zZlHHH90l* z8Ux20b=zoH21qhTX=&J-WnpaBah{k_%&?_{qZn?)E-`0@gSi%=jhv^;hk!!|xT%(R zb@z4M%)P?8dgJ&ye($S3PiOUh7~*Au5P$=a^##(2veffpATc|7J2$7ml(6s_6i#Fy zLdmLRv5~j)7#8 zjQbA9t6BmKMo1$Brvm0TtCDJ`4H2LA_z4*G(8g?FDhqex(0DeRqy1dTaM%|EIUX@7VqL^XG7L2~p9qMntc8gl|vzHRw`1ShJw|{Nh zZOUBG)ul!dYDf!^+q&T4LC-gb6eC@d$2es?uy|3C0 zyrpJ(AteK>4>iYO-lu{IFEr;CjsYOUp=x0-SS4$t3<@XtRPludf>L}(p*wNB7Q-;@ z5N;RSw3WrQxlo zt^IXPI{W9d)miQI`=`*!-<&8PU;XaMHhkk~Z~tnvfAWRtBgd{=b$Pkdfmvh-MVtPd zlNet}Vb=!I2qu7N%;fzpga&0VR8XVm)2INp^aWP8WiaQ!u?n%YGh{`2x{?-1eawM& zHGB?&2e)XaFVMC(v$q4KLbl;c7nGOQxX&?QvlDtTf~Q&&9>B5=CL0^V^HOJkb=UNJ zb6uu}&2}Nno(*7e3vpYWekEmJ!5Om! z+HlS_aB_imO#zq1ysqJnSSQw{LV4b%Or#YFD}cyx++;i%`-BArT`Y^VdZYx54jTGk zHE#5)_8naAxE;S#lFr6NcTO|2fKo>7CfOdY(UIUWO%R=dK`i>kR?`8%?5|Ca2gG46 z*!@+Gv!f;_q$3OA?EsmZb++!V5DInJq?$RU824|u-QRJ01&tjEIo%`Rs9!`DNatxi zlg0^BXt>Rk>x_k(N?JJW`GAY$1sAr~#ug-}`dFXld9Ohsn+(pJuH0DAt(hh10VL`$ zBUbGuxY%(!ddZEDv>u^|Fo@fNmf)>Xqd|x(VBCNJ5M9?jMm!yq;~ z^^D9LFI8oUbV$V!wLuAHo@9rjs_LW#vQfK4iD*;8LLLY?FlG%`(&$bxB!SZJ1kScX ztL?ZQzTj3(S3sbx)zlZb*!KC>8gXKwl}bIl%VL#3W0DJCdeBioG_oXkXzBnoYo!Aj zs=l@&G-`|7oNs$`u$&?MW-)YCQkz7*8)|1eYR|#8*`z{S_AL*{17#r5&Il15&Mpvp z7!`xD12mD9&zM%nD%z~Pazm!QKr*De2xqNH>M2d0+L&0+>eM4AOz?gsMJaWWjJz32 z!Re0Ly_eJgs1+mY)a}N6EKr+mKwvFy%3(_}X{H!A-yn3zbyxFt1ZJ+C&W&YVoIvQh zIcY+fGDL{BGHR)-*+6%&Fq^A0Toq%q$ZpV1cHCY;b4C-c%gQ1vN?acu_n@H2Ww@Fp zwBmtjz8(3FpZ8V0Zo9C^P*^GhP@#dqI+R_pYn2Wjz=X}2VZGG|H^Ib>^i~Q~mJE+? z(e__^$*n0z1QW6ND$%UOr(#or8q6lJ$3$-NqaLuzumEpqq!_@NpwNgncem7xLDrnP znfX>WAhcdRnprMQyUwPGa}MZ|Ler_Z9Rx=^DPMuppepKY)r;nwSfr6#Xr*t6d2gV{ zaLyK3J&|cX_SFpFyHmm(+h)ErWgqG=EwPa{(P?`-gL8i)F*@Lb*0fap8PK|s@^B~R zD`-Y%%-7LyC~wRXo?45xzKmIjVyLlOYVoKV-`z!^Z{VRP112woz>vn204Soq(5{Nw z5u1H1)ihXF3@8U_hv1vgR=Vk2_jcS4UP{@(BYeg?lYZP3ebg3-=>l#VBN{1QNCVn-W27??6@`-dsIwf)Y}aln112BHsDO^B7K!D35?^jy0O_PuBuupF zB=mb))%G{kW{zSspQ4c_qZ!o*E3{`%|KRUl|7dW0xL=(q51&1F|KBb@8XTTqRCfoL zpY2`#m)#Kp|Jlz~5y?<()o z&4Lcoc{eYWjb~zGHSfy;)R_iFr*`Xbjh{BkR%~&Ss zJ<6#CI8qD*ekOskZle|s>Nfq6G);<@2+$Tq5z9#&pa@_`FdQ5VxCIInL5iYAYxGBf zwljO;!)X(szq*3H-JRW?o!goDcKM^+Me2OO>97+hD?fkoncl8tr`Kr`2uYG9qI#Q1 zp?P^|ui#rgb0(yEn#i2H;8h!%h}y{8KD7y^_*J2Ug#AUY@Sey%6gGKiE$GwDG+mgd z**67U(h8I<4+lnjK(5sTERnwxyRhXh!X=4Uv=QV7;lEr=(30T)uLUM}yZu987M47T zlWwC)QqTwxwoz$n5=!GwkUszVFjQ>7>S0-{Rp?;Lj#g6}QAPHAx5t_S8v`UR+&?}D z<5EdcUW|+7jo^#|L^Z|5zKU+rK@x4e95kA`qE0^i$iB(h*p40Qf^FmANdEx* zZ48?!9))f{lrGF3PA5}os>@n_E>nQ2Kr?xL(UQ&{&(K^}mtam<(rr(dHrFi__MC-D zHe;LFOiDv_p-_l=P_1(o^_B+nbP$BxS*M|~L~gsAZn@h(akpRZ7+P}=FHPOCxIcAy zUtT=BxT3AOIxfd(>6;&W2mg&?nB&<>obKWNFtOi}O=gVj zt05+2#?HRm+$qTH_uYQGYDQ1Rg&7CCVXv>XsMb=FKz^-DWt(ar64`QtA5zsOOC|Y; zQaau(LMamuLEr{RD^Y9mkc|mETwXSVeW^O0rF>(r>fjqo<%Ly)qUvNH2Ye3K=*9b2 ziK?6Z#MKP6wqD|-WI|p}c-gUFV03@y{vow?WIZzSW>rw@)cUwt^~B|<)ImZd5hyJ; zlT;_#l((o2a2(A-D|^rF3)SQ;*1oz3v2qmj z?9=T>)0&n%l!mnB(ev5gU{cu4vy`G^DcYi&wL-EmuNg#PdPd7-X+I94cXekxwr6U` z?DpNU>FLQmd+7v@psRV*Q7_I>lBv|}`d+$??XRnC#BkJ2sbnEZkKp7fgrn@ax+iK6 zKvR1dELkFda5S$uT3BEE2G&~tTRFHw*&K6u)NkyDIqx*>h>|O2H)2!F2Gwd?5b!Fo zAX;(YsxO2%HU#*7RGAD)?~nN=ebNW@0<=Hy$&@y7-HJ&W~tMO@Z5e+pA(`|4u zJe5hM(phfqblA=frM;*CcsoCrJea0PW>to8Q=O^wT)L1hSxdRGnEYXx=5VzsIbS%e z%Z1#LbXFI0M+`e z1~?dAswlu*VkVcTObeEhthPZF%iLxyNga(s-y8n>xUBN)(7 z@^+vSuj-52*+UQbBn3zcDWiqG)#@Ab=aq2;v7bSB2H^;^ zhT4QI>kavZJev%Ci>-!wc_QidgYYmYLGMJUsakx`su@KQ5!%Zl`Zx*$|xh+kBYm%}*WI0Db)vK_jV%NOXgIB>s3 z^i`CdU{~7q)UH5NYZNOaRP1X%F37E%fv$C2OeJB$J59TKk%17KVy#fEj#v=KQFPTE zLfnY>ejUe65omwV=0Mz32l$~m)&bSEUS+pWx;E_F?U6om3ef(@Bk%SV-`n7=q9)*N z{v`$-$@s>+f*c%Cdg$}rUK?mqFtCqOTt~B@-&8qdw_e|86cH3tzB{LF03sG%(^SBvw@wzJ4 z?79tNGLy^VP0TZ_!sH8e<)IKwADhq6bSkxv&cP;SW*@{(C0#1ctt5%*c&?DH;&O}O zlS-2Cgm?)Q!TqHjf;33>ZLj?t`r2Cn<-7Q5-=_6@xH&S{7lxq=(FvLSIwk^M8J7W9 zWo2SWT6Oy-qS9(uz;;xg7-GMSj0~RnQU@o{eqO4aBqmJkPB`F!pHD;;$|NYor5PKG zHF($_uhMTy@E@AEw%gD^Qrq!y6t5Z?Jpd1~QIN6=4Iyy5#GZ_{_L(a0ysT|~Ex{jL z;A;N9gtk?+@pkpX#s6g?fxGuKD5oxjGXP;huR&j?HSh_NMU>Z?l=fUioLxoGC62 z7DXY(HVr=}JkQP!-wX$65e9V2>})D`aCVl0KMaP4XM{%gVDD=PKV_#M>`*v+J%Rlg zd-uWiw#s4bLZOO)8_@;;Ajcy6^MgM7ub=}#Mzrs++L2SXE%*HROYNSOEsxay8*;9B A-2eap delta 4165 zcmd58m!gOUM;9BT>tmLdp!2SjkJ(mLe(!MN*7CLYQmjQdKb*x6v_o zBy8GbjTzS@d3w9vOMmaS8j6D6k*tcXPoPVICi=i_r+4Z-yq!8d&Afx|@C4>IQ(4bc zid$FAm~wGkkW)5W;lvdwch&Bs*vIQ_r6G2OdH#R8bUDzN$3tr}L6W zNHx(X&5e8u{jZ^k7R(mD4O?@Hdq-1sCdU<}RH)jEhpVl%dOR}Tr+E4ex}<0XA9xjC z(x@~hr5-WNg}Bs~>TDyIg$M|8MU&!xtS=H$s(NHx!c;wh9${8!;S@*FG-L{|)Hy!K z?GszL94`knt8(y}uO|+shet=1|5$eQ_IK}u->&<=gd-#)^d0Zj=WdPNcY-Nn5P{D_ zozUuL)KV!krKWRPf=1r|Crp+-1fq@8OltmIjui5mIHi^e#xTK;)QqKaraW8H45v$l zJQ<*ud>+eo9L1=mZ<}RW@kM;7PBUaPy+H+tbi2y#zbWb6?m!7tktddY`|kM;F8K~F`=0(t z{M2`p9`h^Dd=0eSHZ&d_D)}NEDQQKoW*Y#0Z3@(m6I!x3Fsdp&jOCg zHvQzJv$HCBkxYV26K8U8g1LN|oPaLzqs#^enqF0qLph!-$slxc-QWYm+K!rGrZiO| zGcZ`fX(1@YgHz7|Xc3x;1njjMrz=CvCxsC-R5whs94e6GxolR=Gjqq}I^EDZTy{#$ zWL2V>h+3S^oKQ(Cs@CnHe{Ko%Gy9OSWQn|hI~p>xr^PbDQ`KxGH)@adn9i;AI)2?4ZPJrC)xwD5Ce+aNOK!z zyI^*tWiu*u(`)VfHl5+2|89@&*-VEQNvDkqa~7_ZOJT^d68Gus=|}dY4PG~ONM^>L zj7eRJZykRc^^3&bSf#R}Nte|76+@q(>nxgeSs+j&v!pN(E)iu^qac<5oG=syH9Zu~G_eVpbB?s$DA-+fuX+S%G3%T|=?0Brd}WatuL1 zSj#Yy!2WXxNrZ9wdbpkcCjD)=iJzdKg}*J4Z-JbSZP~`yMXNhO)J(ZR(zzn}A+Gol z!g+)X2tNi$Xr^LedK$=iOm?$`C21U+L|Elxoi&c5Yyg4L4CCW0c2$HB!byZF4(!ue z$ehV$)3^ay%WxaJ2FPig%2;sKHcc!P%j66WthbW9in19x5INe0A5j_wmp+!+w^VLL z6h6}WrRcq3>lGD04Cwvv0Ma{Rbhr(wMY{>ni=xHO5a9bJhFypsSfYsicEGz97h>?K zAXFa>SdVmx9~%Nk!s5p*JhsEqk*^%d+`bt1(%j6y|S^JNq;Mi-%_6FR*n@8<7R{vf}_hjC|}yT9Xz!}g(kp&U9{ zn9XMsnt}Ojg8poK^KN!ythrCropE)Yb|7W_x_Ax5T034Bt44l88+QCovZ2RTUY9nB z$Co2LyE91rQMV){g#n(v8{B3~*df%-M>@AtS6~ zbxo?;NgV88nW|YJZgYh^K5T5kwI}W#$$U>8WpK|XTQ{08y0px zWdt>>#Lc?@>#Bl>{BN)NpZ+nxE{C8Ztcw7l=C6qpborb$tS=@-h@eaL?tM~?)UbB{ zY+czl3$6@u$(oTE_$?blX*I-*v|(3U)RbC%(B~ahqZKoYHuh%;+VDRo$=d6d!b9OJ zER|~lk3|xB33em(2;T*edCi8Arj{Tkkoq5rNImdsLoTB8%VT5gWmG*!n|j+iF&O3A z@M4kmeVp(L0>&75gQk1E#_Q13!oUPg$EFJ}?-rPHAnZ str: + forwarded = request.get_header("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + real_ip = request.get_header("X-Real-IP") + if real_ip: + return real_ip.strip() + client_id = request.get_header("X-Client-Id") + if client_id: + return "client-" + client_id + return request.environ.get("REMOTE_ADDR", "127.0.0.1") + + +def _hash_ip(ip: str) -> str: + return hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + + def _check_rate(ip: str) -> bool: now = time.time() cutoff = now - _rate_window @@ -70,7 +87,7 @@ def register_routes(app): @app.route("/api/messages", method="POST") def post_message(): - ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + ip = _get_client_ip() if not _check_rate(ip): response.status = 429 return {"success": False, "error": "Rate limit exceeded. Slow down."} @@ -97,7 +114,7 @@ def register_routes(app): content = _strip_xss(content) - ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + ip_hash = _hash_ip(ip) msg_id = _db.add_message(channel, ip_hash, content, gif_url=gif_url) return {"success": True, "message_id": msg_id, "ip": ip_hash} @@ -134,7 +151,7 @@ def register_routes(app): @app.route("/api/polls", method="POST") def create_poll(): - ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + ip = _get_client_ip() if not _check_rate(ip): response.status = 429 return {"success": False, "error": "Rate limit exceeded. Slow down."} @@ -172,7 +189,7 @@ def register_routes(app): response.status = 400 return {"success": False, "error": "Option too long"} - ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + ip_hash = _hash_ip(ip) expires_at = data.get("expires_at") is_multiple = data.get("is_multiple", False) is_anonymous = data.get("is_anonymous", False) @@ -217,14 +234,14 @@ def register_routes(app): @app.route("/api/polls//vote", method="POST") def vote_poll(poll_id): - ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + ip = _get_client_ip() try: data = request.json if not data or "option_id" not in data: response.status = 400 return {"success": False, "error": "option_id required"} - ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + ip_hash = _hash_ip(ip) ok, msg = _db.vote(poll_id, data["option_id"], ip_hash) if not ok: response.status = 400 @@ -236,8 +253,8 @@ def register_routes(app): @app.route("/api/polls//close", method="POST") def close_poll(poll_id): - ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") - ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + ip = _get_client_ip() + ip_hash = _hash_ip(ip) auth = request.get_header("Authorization", "") token = auth.replace("Bearer ", "") diff --git a/index.html b/index.html index 11a8663..5ee84bc 100644 --- a/index.html +++ b/index.html @@ -524,6 +524,21 @@ let selectedGifUrl = null; let votedPolls = new Set(); + function getClientId() { + let id = localStorage.getItem('clientId'); + if (!id) { + id = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36); + localStorage.setItem('clientId', id); + } + return id; + } + + function apiFetch(url, options = {}) { + const headers = options.headers || {}; + headers['X-Client-Id'] = getClientId(); + return fetch(url, { ...options, headers }); + } + function loadVotedPolls() { try { const saved = localStorage.getItem('votedPolls_' + currentChannel); @@ -560,7 +575,7 @@ const url = query ? `/api/gifs/search?q=${encodeURIComponent(query)}&limit=20` : `/api/gifs/trending?limit=20`; - const r = await fetch(url); + const r = await apiFetch(url); const data = await r.json(); if (data.success) renderGifs(data.results || []); else gifGrid.innerHTML = '
Failed to load GIFs
'; @@ -695,7 +710,7 @@ pollCreate.disabled = true; try { - const r = await fetch('/api/polls', { + const r = await apiFetch('/api/polls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -748,7 +763,7 @@ async function loadMessages() { try { - const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`); + const r = await apiFetch(`/api/messages?channel=${currentChannel}&limit=50`); const data = await r.json(); if (data.success) { window._lastMessages = data.messages || []; @@ -830,7 +845,7 @@ if (votedPolls.has(pollId)) return; el.addEventListener('click', async () => { try { - const r = await fetch(`/api/polls/${pollId}/vote`, { + const r = await apiFetch(`/api/polls/${pollId}/vote`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ option_id: optId }) @@ -861,7 +876,7 @@ sendBtn.disabled = true; try { - const r = await fetch('/api/messages', { + const r = await apiFetch('/api/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel: currentChannel, message: payload, gif_url: gif_url }) diff --git a/models/__pycache__/base.cpython-314.pyc b/models/__pycache__/base.cpython-314.pyc index 584c546ceedc7f9ae1775bf9d062f8b3877c1f8c..25c80b8bfdaa36be2c1828cab0c878bdd1000bb8 100644 GIT binary patch delta 342 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 W2>es%o)x<1tz#moyDlK*^8W`!nNbt~ delta 3758 zcma)9YiwLc6~42(_wLiXYw!BKw%4zz23dHZ?Z4X zy$(19Hk7t%2sCj=6={)ZLlMG7rBxQBMnHu_2#KI76|J!D4}|zpMDv3*6?Ief2j|R< zA6TlKYx$eknKNh3`OeJj%`@chr-`@ITjCPn8~w$^@oe*gw~9Qu(01g0GG;$eGUC;} zBR<_X;@ABnrF!W|nO@c)lnaf*n4?k{bMCU>OhM2CW29M-T?<#fQ6d`dOyYzRSF+P; zmL*Ay_=}gwr-YQUSFIltJNudKK+q49XeoeZ1Bk#!%b6%Pfl#{`t*wI2Bmr3c>`<`^ zg5$$+_C{bg`?dHCX=gF1$4UQ^ej8{(B(ceIODdPawQxX7CG z42dfG9L>rWT80C-d#DGn9t6-ZJWZ!f(<-nREo)>y^>mUd_HxtBqVLhSD-;GT;Y$~W@vbUCm*^qb0h5=&#uta+)T4dNW;GD#?LU%)Jkr-A@r&}8m zVI{s6Fl48Xk$QH`*H9LQ>2wf)Th2+b;%g!iR^#v9yrxshCh2@$S5kacIt0_UUQ=do z`f<(M{x=_}hZ%-FrEoQL6xTk8UCyDeCzZSo!3KR{?EWIax=A0EHW8Usmn{#$fTcfpJnef?6Iy_!7dG5pWg5sjJ{> zJ(0<;(w91L@bd^qQNo52hBc8&(e2o_s3{Yet*aP+276z=XuGvmckY zlkMzJat<+Q5Z!;X8|dlf*xVp-*kwJ-q+KhrM5md~Zr^s68v zSD3jf&dw=xp6}U^rqr~qP(Iyo#|>E~G^fyd*jNnc=!>BPxgF1u&k)>4uz zKG6CV(rfx1M9h@3th;?{Kc8i9MOw_`!@}}nuvpUeA5xeBWih$Lbi#4e%sp`eYux+j zSeiySgOEYs49{UrL!bb%w810%kPvYL8l6K4+`lFPnET|HgyDqDVvoupa<+Iuwvz$A zIUX6#DXU2$en8q%YVsb=iwo?Z9bG^rUuOp)Gi+~H&7O6_^L_E%aC`5vov*hRqm69; zepeH@z+USrKFkf{HW7LO_qyf1`2y}2uhoxF3oAmc==uO*Da8v~Rs7|Bk6Q|@Fdj3B zg}k6=0SxhEPFFNT!h&j>62J_>CW7%TA@dBkp3@WQIM*PTayA;)w34N0j^W@c<7(av z+C%}@JdCim`n@hEyUbv(`Heo0D|mp|a1(0g33m}ypTm9^t{hM1=CZm$PSO{lPjd+Y zllvQZPboXFqk+6oRCg3eVT}%k)6|RS-%F?zL$PQx>4BH1A?kDav~rK<&2Mn|od7&G zU84Mf0Px(vEi4wz;&RWe3T!UjeVP2nh>Y3iK4;>M7XR zq0jAQH-l|1FMSI14#(&*R=#Jdbp#qR;SrXthGknfcL!ZW1MvM73B!Afy|`y5`6K)5 zp8MRKi#SSOV(z_{3IQ`K^VqY_ULOASGip1G!gZCMH%}jaA|A&XZUmmkTrYag5T{fM z?=8MY96DA5xnX6TWyX~q+_zuiPu0up{Jx;X9rt?i<$V`PVc8Qn7dsPs#}ir(w68ou z_|x><_*r^m~JP7n`O;Ih{sW02lR5Bf5|)_M9q`$OL@ zI50lx`yQ#s*wI(`yx~*cLvVV9vpGFH1-wnsBKyfh`v*fPvMChNhvYXM|SEGAnF`?Qi(Xd?Y@OehuMU2Pa>;m{ zj Fernet: @@ -228,7 +242,11 @@ class DBfrontend: voted_at=int(time.time() * 1000), ) session.add(vote) - session.commit() + try: + session.commit() + except IntegrityError: + session.rollback() + return False, "You have already voted in this poll" return True, "Vote recorded" def close_poll(self, poll_id: int, requester_ip: str) -> tuple[bool, str]: