From b8182ead888287cb51e30119e34d95d0897afe68 Mon Sep 17 00:00:00 2001 From: Slfhstd Date: Mon, 23 Feb 2026 22:48:25 +0000 Subject: [PATCH] Initial commit --- Bot/__main__.py | 28 ++ Bot/__pycache__/__main__.cpython-39.pyc | Bin 0 -> 1100 bytes Bot/__pycache__/main.cpython-39.pyc | Bin 0 -> 5493 bytes Bot/bot/__init__.py | 1 + Bot/bot/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 195 bytes Bot/bot/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 169 bytes Bot/bot/__pycache__/post.cpython-311.pyc | Bin 0 -> 1350 bytes Bot/bot/__pycache__/post.cpython-39.pyc | Bin 0 -> 862 bytes Bot/bot/post.py | 24 ++ Bot/jsonwrapper/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 211 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 185 bytes .../__pycache__/autosavedict.cpython-311.pyc | Bin 0 -> 8025 bytes .../__pycache__/autosavedict.cpython-39.pyc | Bin 0 -> 4425 bytes Bot/jsonwrapper/autosavedict.py | 120 +++++++ Bot/jsonwrapper/tests.py | 146 ++++++++ Bot/logger/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 200 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 174 bytes Bot/logger/__pycache__/logger.cpython-311.pyc | Bin 0 -> 18888 bytes Bot/logger/__pycache__/logger.cpython-39.pyc | Bin 0 -> 12178 bytes Bot/logger/logger.py | 320 ++++++++++++++++++ Bot/logger/tests.py | 123 +++++++ Bot/main.py | 250 ++++++++++++++ Bot/main.py.bkup | 228 +++++++++++++ Bot/sqlitewrapper/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 206 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 180 bytes .../__pycache__/model.cpython-311.pyc | Bin 0 -> 15619 bytes .../__pycache__/model.cpython-39.pyc | Bin 0 -> 9901 bytes Bot/sqlitewrapper/model.py | 309 +++++++++++++++++ Bot/sqlitewrapper/tests.py | 83 +++++ Bot/utils/__init__.py | 2 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 231 bytes Bot/utils/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 195 bytes Bot/utils/__pycache__/actions.cpython-311.pyc | Bin 0 -> 4357 bytes Bot/utils/__pycache__/actions.cpython-39.pyc | Bin 0 -> 2552 bytes .../__pycache__/constants.cpython-311.pyc | Bin 0 -> 431 bytes .../__pycache__/constants.cpython-39.pyc | Bin 0 -> 327 bytes Bot/utils/actions.py | 99 ++++++ Bot/utils/constants.py | 13 + Bot/utils/tests.py | 48 +++ Dockerfile | 16 + LICENSE | 21 ++ README.md | 0 config/config.json | 11 + 46 files changed, 1845 insertions(+) create mode 100644 Bot/__main__.py create mode 100644 Bot/__pycache__/__main__.cpython-39.pyc create mode 100644 Bot/__pycache__/main.cpython-39.pyc create mode 100644 Bot/bot/__init__.py create mode 100644 Bot/bot/__pycache__/__init__.cpython-311.pyc create mode 100644 Bot/bot/__pycache__/__init__.cpython-39.pyc create mode 100644 Bot/bot/__pycache__/post.cpython-311.pyc create mode 100644 Bot/bot/__pycache__/post.cpython-39.pyc create mode 100644 Bot/bot/post.py create mode 100644 Bot/jsonwrapper/__init__.py create mode 100644 Bot/jsonwrapper/__pycache__/__init__.cpython-311.pyc create mode 100644 Bot/jsonwrapper/__pycache__/__init__.cpython-39.pyc create mode 100644 Bot/jsonwrapper/__pycache__/autosavedict.cpython-311.pyc create mode 100644 Bot/jsonwrapper/__pycache__/autosavedict.cpython-39.pyc create mode 100644 Bot/jsonwrapper/autosavedict.py create mode 100644 Bot/jsonwrapper/tests.py create mode 100644 Bot/logger/__init__.py create mode 100644 Bot/logger/__pycache__/__init__.cpython-311.pyc create mode 100644 Bot/logger/__pycache__/__init__.cpython-39.pyc create mode 100644 Bot/logger/__pycache__/logger.cpython-311.pyc create mode 100644 Bot/logger/__pycache__/logger.cpython-39.pyc create mode 100644 Bot/logger/logger.py create mode 100644 Bot/logger/tests.py create mode 100644 Bot/main.py create mode 100644 Bot/main.py.bkup create mode 100644 Bot/sqlitewrapper/__init__.py create mode 100644 Bot/sqlitewrapper/__pycache__/__init__.cpython-311.pyc create mode 100644 Bot/sqlitewrapper/__pycache__/__init__.cpython-39.pyc create mode 100644 Bot/sqlitewrapper/__pycache__/model.cpython-311.pyc create mode 100644 Bot/sqlitewrapper/__pycache__/model.cpython-39.pyc create mode 100644 Bot/sqlitewrapper/model.py create mode 100644 Bot/sqlitewrapper/tests.py create mode 100644 Bot/utils/__init__.py create mode 100644 Bot/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 Bot/utils/__pycache__/__init__.cpython-39.pyc create mode 100644 Bot/utils/__pycache__/actions.cpython-311.pyc create mode 100644 Bot/utils/__pycache__/actions.cpython-39.pyc create mode 100644 Bot/utils/__pycache__/constants.cpython-311.pyc create mode 100644 Bot/utils/__pycache__/constants.cpython-39.pyc create mode 100644 Bot/utils/actions.py create mode 100644 Bot/utils/constants.py create mode 100644 Bot/utils/tests.py create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config/config.json diff --git a/Bot/__main__.py b/Bot/__main__.py new file mode 100644 index 0000000..c90c051 --- /dev/null +++ b/Bot/__main__.py @@ -0,0 +1,28 @@ +__author__ = 'hor00s' +__email__ = 'hor00s199@gmail.com' +__version__ = '1.0.0 beta' +__description__ = 'This bot will go through a sub and\ + alert mods through modmail if a post has been deleted' +__license__ = 'MIT' +__dependencies__ = ('praw',) + +__disclaimer__ = """Disclaimer: +This Python bot is a personal hobby project created for fun and learning purposes. +It is not intended for any commercial use or critical tasks. +While efforts have been made to ensure its functionality, there may be bugs and/or errors. +The bot is provided as-is, without any warranty or guarantee of its performance. +Use it at your own risk. + +The author and maintainers of this bot are not responsible for any damages, data loss, +or any adverse consequences that may arise from its use. +We do not collect or store any personal data or information from users.""" + + +import sys +from main import main + + +if __name__ == '__main__': + sys.exit( + main() + ) diff --git a/Bot/__pycache__/__main__.cpython-39.pyc b/Bot/__pycache__/__main__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..566b51cd41e2835f28e75f93a1ce8b455f74007a GIT binary patch literal 1100 zcmYjQ!EV$r5KVTMHcbmu;=*yRmC9~=;82861wGIMs8WkSxkTQ0c4O+;!FF2GD?b7^ z_zwPJublV=F7TX%imYOSqamK@{tFEPlZNMu^1 za#W1uxERYxF_CwQJ94Agkef2oo5d#PEuA%4k);W5$^3HUPI&Wk&e^BrOPVG!J5Tnu zi;dd4g9Cr_U_Bom9UU(lq4cz}%}stdJ(wQQoP)S|cTp=(a~tSdX-!K@p?0=g)+ESx za}q|9(Ch*=R{Gl{77>or0^haP2dV{Nm<>tR99S0PSEm;V-fIZLKsDFTfEzYPsTdF^!N+!4eza zIVOc9Bve5~ohtPEaD4Fio%3{r2IZChXGx{ zj-IZCbHao^s#|s<{%1&C44iNV${JxRp5|{*g2JXiz3m*?YeP=?%V|DPi4K@%DX} zi50D~vW#?Cay*n}x!Nwvs1-Pf-!Dta9vyEZFapSGRiUMf#cs^D z@w}Kyu}9<{BDY5sU}N8Vhx406W+NKxT21()3f~g9h43)^dOWLb!?RY+&}=_DS>J*) zTn#^a;lv`GYes9H+c5j5f7?y8Nr}xb@BprSujHAg;mhOzR82SANO{8Ze literal 0 HcmV?d00001 diff --git a/Bot/__pycache__/main.cpython-39.pyc b/Bot/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e6e1b843d2c8cfb6c99ee8b2d792219cc77e2f7 GIT binary patch literal 5493 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 TAFJ-Xy)*|T{|N+v*?;&ye#lCv literal 0 HcmV?d00001 diff --git a/Bot/bot/__init__.py b/Bot/bot/__init__.py new file mode 100644 index 0000000..970613c --- /dev/null +++ b/Bot/bot/__init__.py @@ -0,0 +1 @@ +from .post import * # noqa diff --git a/Bot/bot/__pycache__/__init__.cpython-311.pyc b/Bot/bot/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6c69a14536d218f979cd67b997d188f92ee7482 GIT binary patch literal 195 zcmZ3^%ge<81Q`N{Q)GbjV-N=hn4pZ$d_cx@h7^Vr#vFzah7_h?22JLdj6gw6##@Y9 zen66?Aiua|CBtWsj$fAg8Tq-X`URQ#PWdIp`Yx$CsU@i?0YDYS`p!j(X(hoqsih?u zKuP^1AdZjE%*!l^kJl@x{Ka9Do1apelWJGQ0W=9@MX@-L_`uA_$asT6_yQ^_Vgt$n E0J6?7jsO4v literal 0 HcmV?d00001 diff --git a/Bot/bot/__pycache__/__init__.cpython-39.pyc b/Bot/bot/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..987b1d6b8cd7d5e9ada8f0bdb08c9c31693bb666 GIT binary patch literal 169 zcmYe~<>g`kf((JfDKbF%F^Gc(44TX@8G%BYjJFuI z{D34&L4I+`N`@j9pdgs|WvQQ$pPQ;*kg1=PUs9~^lA4oRlA00#R939-T$Gqr5}cD- oT9V2KczG$)edCEXCP((0G56zj{pDw literal 0 HcmV?d00001 diff --git a/Bot/bot/__pycache__/post.cpython-311.pyc b/Bot/bot/__pycache__/post.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b414883c266d7221babd4f31bc5b9d06239496a3 GIT binary patch literal 1350 zcmd5*y>AmS6u0jqcWILVQc(l6Gz&$MP&2W$2tk1bs;Xr=RkALzqg-{#r9OwYC_{%z z1gS6;l_5h1R6u25Wal5~z))Grgc#Tkg#mTq`7Wu1fPsy7_VfF&@BRGV^XqJO1Oe5~ zexxl8q0dSgk~t9icR|=g6jAJ>I(9HtvgT`b-O=lYW7JK@1e@-pG;|M9V;fPE=?6MO zhcFXzESg$IMeD0li&z?quBa7tX+8)j^QArS3Ri44nbenp^`g#IaA__Ep$K^j<2>ktxnZi#HZ3rKnJiM z4i&U&48=BXYgNrdZM1rN@)dp_S01vcEfj5Ow?f9_B$~n#K9h!E8$w!5s2TC7JOg!Q z!V4Ot&O|MsG8cP%7u<|`LgqND1e}rzXRcsWW(T@*GFmi~9?+K`F3Bv3JAn}4mLZZE zwwjDfn-H(z2||j7G(zTAxhfY|9WB#@R3KDHNT_aAC=|9%u9s>-ot2ti=~f`Z(!9dQ zX#D5WZSGdZvd>zg2F_9$rh+@u+~j#U|2P+Kn;AyTf-R)Ler%yH`9e3-J3qO*(i@xD zz26(3+HLoyu6EnK%hO#yVUt(7E77@hek4M`I>t^W8bj#|&&;RRj`hjTzZ~B$9@(=n z-iUYYL^|bthyzbY-B;zC5II6fJ)kXL$r(b{TCP9v=%L_Tb!-e95|^Z*!h9d{g!x5q zg{p6F@=I!$YKE9)lw%1P+-Uek+Ne3VuSW=$_eQ*Y7>k#GVtis4MD>lOUYqZgW0W!* zuK7^zb?!Eyq+Hc1u6z9GrA3IVfX4{mqU1*k_lZ55B;W?0231`sYyyjPjPWtbAN@Lx g(ah1`N0c%~wJj^kW$;{terKIK-<0+HA6zk_pR?LI(*OVf literal 0 HcmV?d00001 diff --git a/Bot/bot/__pycache__/post.cpython-39.pyc b/Bot/bot/__pycache__/post.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0b618936f313d0dd568a144a1942475689b9b3e GIT binary patch literal 862 zcmaJIYjpntgJzmr z*7BNj$}^j_q86>JWt-PIeiQZR9n(_hH%#Y7T#LHUqcb)seqcA7@F-7ISWQIC-*?*B zSk6>X;i@-LoOYKJ;Rr6~C!G)8<(Mz1Z`Wu8nSX}Cu$pUDXGR#Qxz6syEn9Lew7l7` z^IIlaP3XBB;pSt6E%Y4Lsx@)XtB(eHBojx@gu$&Bg2rx*@;!8V`x%D7&iVgb^Sg4x zAPeG}oy!g1WXr5!8+M+33EPTR$K-=lTQoavxF(|vO z2p^ooTgnnbthQ7a###XPgbFD None: + self.__table = { + 'username': Datatype.STR, + 'title': Datatype.STR, + 'text': Datatype.STR, + 'post_id': Datatype.STR, + 'deletion_method': Datatype.STR, + 'post_last_edit': Datatype.STR, + 'record_created': Datatype.STR, + 'record_edited': Datatype.STR, + } + super().__init__(db_name, save_path, **self.__table) diff --git a/Bot/jsonwrapper/__init__.py b/Bot/jsonwrapper/__init__.py new file mode 100644 index 0000000..abf7a4c --- /dev/null +++ b/Bot/jsonwrapper/__init__.py @@ -0,0 +1 @@ +from .autosavedict import * # noqa diff --git a/Bot/jsonwrapper/__pycache__/__init__.cpython-311.pyc b/Bot/jsonwrapper/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49530608af7466d1ffcec206ea170a5973382472 GIT binary patch literal 211 zcmZ3^%ge<81Q`N{Q&fQTV-N=hn4pZ$d_cx@h7^Vr#vFzah7_h?22JLdj6gw6##@Y9 zen65Zv9u(=II%1>B{R8XCBtWs&R>rD8Tq-X`URQ#PWdIp`Yx$CsU@i?0r|y1j&o6B zT1jwDYH3LZP*OjuI6tqvD6ya*wMaicJ~J<~BtBlRpz;@oO>TZlX-=wL5eLvjkd?*a VK;i>4BO~Jt2I&i^sE7?H2LQv9Hg`kf((JfDJnqvF^Gc(44TX@8G%BYjJFuI z{4^P(coIuX@{1G8Qd2UMOI9)zF#{#R#4ktvjQreG{en#Wr2LX%eV5dn)RNScfc#=0 z$GIpmtt2=nwX`I|DZfNNt2jTeyeP4tAhk$8K0Y%qvm`!Vub}c4hfQvNN@-529mvYh HK+FIDiZw3b literal 0 HcmV?d00001 diff --git a/Bot/jsonwrapper/__pycache__/autosavedict.cpython-311.pyc b/Bot/jsonwrapper/__pycache__/autosavedict.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d122d96a6a85a10b1569721329579391ba663fe GIT binary patch literal 8025 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 literal 0 HcmV?d00001 diff --git a/Bot/jsonwrapper/__pycache__/autosavedict.cpython-39.pyc b/Bot/jsonwrapper/__pycache__/autosavedict.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38d99b05124528a31cc841aace9cfd0859aaa6c1 GIT binary patch literal 4425 zcmZ`-+j85+8QxtiK#HU&N|xnA9Y=|s#7tbtcAT@5`k*vPCs8w_&Sb*WMG-DoQ6>p` z0os*@x{_ydmG%X6AfmQgR=*o0)XCQ1z{KU&%j(bim{LoUxEKxMD!|p%Thb@=*N3THJCy51jN@P{l*XQs-3ZS*0N@ zt(>i_sCm#_RfS9z4uz_z#b>hNsU_??gYmLDi*W(tl|!!1sq+{W!#VYlx`1;?q8AUP z`klJ;%st?Z5@tTeSww&B&{3Dw>y};y{S&on>2HAkrn+M36?}!xy{g{Ax%1iRZS@XD zRrRS|e+^vJ)w|E!#)8E&=KMW%9h}$H@96}KZ%wU(vY4J?DL2$jP?o^aE%kne__q20 zv@^dj$r{Vh<7b+zYIE=6ST{dK@!I!6vi)uQQTP*d`S783u%a%^T&vv){XXW5+Yj10 zZi<{{XpzKQCKGI+Gvq_&v!OtX-VV%g!1QH17QNNP*^x&s(MLSKol#3ecI1s1c8H{q z6o>3U?98=T8Lz<}G&2H|r&h4>7ai=dH!}RoDs!oy2(H6C4^5#YS3|z3`vy zIEjtx>o8EpjR*ZuTkxjn`|Vyk@%_4EWE^%{_H13j2adU!^ZjNgh-2SBX8-$geLL!g z>;3ln&ruSuZ-kvN3Dx%zPF{bYgI2QD2?xpc{U}-AiKE_A4bu1O$&1N`|J@F%B= z^uWtl{F3DnLW}4sIvFAXMEvs{TfQ=eae7X4Ffqw-osBCg~&yX>_mYYqgkW5Qxt2x@?%U+@IbFi(|V=Yw>Lk|zBazsr8zefJ;4q^lc2OX$rr6R^7xegZtVmtXk!j4B4w&P02$i$xp_^7BsbsCwksikn z!I?lJr-M%U0Y_+ryIBxU4g`!)Qap#Y$cXfZ)L2=aq8n2tuCmX6iIu76$>MCl%Prfh z&8A`?a$?;yj*2r6%(L&}*ojI05d&gB^S_JxPQs56;bD;SCC|H_H^u~+U`k^YD8?p9 z)~Ih%LuEtVO`(56gion?m2IZlqXObvEK3d4sq$;H$&TRDs%GBTs82v2P%wNKodkge zVAu}e#Fr56CHgLf_})ng;hrR{f~GvH7pYmI#;VCieH%6@ogMQu+Rp?_NtMAC<};_N z@~YTdoN_8!@oIiou{TXf2x^pCQOXdf=sP0>x)Uy?ahshm(8e3|kuO7C#a2z$uJ2GY z!&yOxaT4k9N6cl+6?uj4El)9*Ekc4gapQd1Mn*HSdD0%-7?P!u0Ipp7tJrto){=?? zxN{}pLvD>DOFwBrAdE{XGaeg-&Hg8@a2v-O2PMfY1lf%GyMH78$!?jDh{1?i)8eHy zug>?^urFno0xiUgkZXmDko+5El%T8MX&-%G?@ZA!j4 zWpBaWb58Q)lrfOuW-pI#U_goDh>zHj7_kHR=LNP8qZlWPM1o(J41?;f7G1T(=JOYL z5HHvmtB`FkYVlk~;3gCp4FwPQI=&r2;XK{UvO^@(qxR!wmxTftUJ8cTe*bf0` zT$CKaUaXio*ao1yJv%w^enEU%E1s#LXyP&PLPlsSk?D+OJwnf64mXh6OrGk#T!zpw z^jaP%Rt$O0mSN2(auLlbH6@nk)L>4$(9!p>%gR*$0ljfC`zQ1be`NTRS#i>;0p5Jt zoi3+Rf3|hnrY+Y{vEkoYx_R`1Zs_}_ zHgr+cGG%)=>4wR6r1S?gM-_nnj2as{?o#goH5=5B?zPnhv2EtF%S|)vbbLSOpH@w_ zOs7CkC4Nkax+vI-mFkMO?A2t=t-81uaQ5PSK@i None: + data = self._read() + data[__key] = __value + self._write(data) + super().__setitem__(__key, __value) + + def __delitem__(self, __key: Any) -> None: + data = self._read() + del data[__key] + self._write(data) + return super().__delitem__(__key) + + def __or__(self, __value: Mapping[Any, Any]) -> AutoSaveDict: + data = self._pairs | __value + return AutoSaveDict(None, **data) + + def _write(self, content: Dict[Any, Any]) -> None: + with open(self.file_path, mode='w') as f: # type: ignore + json.dump(content, f, indent=4) + self._pairs = content + + def _read(self) -> Dict[Any, Any]: + with open(self.file_path, mode='r') as f: # type: ignore + data: Dict[Any, Any] = json.load(f) + return data + + @classmethod + def fromkeys(cls, __iterable: Iterable[Any], __value: Any = None, + file_path: Optional[os.PathLike[Any]] = None) -> AutoSaveDict: + data = {} + for key in __iterable: + data[key] = __value + return cls(file_path, **data) + + @classmethod + def frommapping(cls, __mapping: Mapping[Any, Any], + file_path: Optional[os.PathLike[Any]] = None)\ + -> AutoSaveDict: + data = dict(__mapping) + return cls(file_path, **data) + + @classmethod + def fromfile(cls, src: os.PathLike[Any], + dst: Optional[os.PathLike[Any]] = None) -> AutoSaveDict: + with open(src, mode='r') as f: + data = json.load(f) + return AutoSaveDict(dst, **data) + + def init(self) -> None: + if not os.path.exists(self.file_path): # type: ignore + self._write(self._pairs) + else: + self._pairs = self._read() + + def restore(self) -> None: + self.clear() + self.update(self.__default) + self.init() + + def copy(self, + file_path: Optional[os.PathLike[Any]] = None) -> AutoSaveDict: + data = {} + for key, val in self.items(): + data[key] = val + return AutoSaveDict(file_path, **data) + + def pop(self, __key: Any) -> Any: # type: ignore + data = self._read() + data.pop(__key) + self._write(data) + return super().pop(__key) + + def popitem(self) -> Tuple[Any, Any]: + key = tuple(self._read().keys())[-1] + value = self.pop(key) + super().popitem() + return (key, value) + + def clear(self) -> None: + self._write({}) + super().clear() + + def update(self, __m: Mapping[Any, Any]) -> MutableMapping: # type: ignore + for k, v in __m.items(): + self[k] = v + super().update(__m) diff --git a/Bot/jsonwrapper/tests.py b/Bot/jsonwrapper/tests.py new file mode 100644 index 0000000..cccacbf --- /dev/null +++ b/Bot/jsonwrapper/tests.py @@ -0,0 +1,146 @@ +import os +import json +import unittest +from pathlib import Path +from .autosavedict import AutoSaveDict + + +BASE_DIR = f'{os.sep}'.join(__file__.split(os.sep)[:-1]) + + +class TestAutoSaveDict(unittest.TestCase): + def setUp(self) -> None: + self.path = Path(BASE_DIR) / 'test.json' + self.default = {'a': 1, 'b': 2} + asd = AutoSaveDict(self.path, **self.default) + asd.init() + return super().setUp() + + def tearDown(self) -> None: + os.remove(self.path) + return super().tearDown() + + def test_init(self) -> None: + asd = AutoSaveDict(self.path, **self.default) + asd.init() + self.assertTrue(os.path.exists(self.path)) + self.assertEqual(asd, self.default) + self.assertEqual(asd._pairs, self.default) + self.assertEqual(asd._read(), self.default) + + def test_restore(self) -> None: + asd = AutoSaveDict(self.path, **self.default) + asd['c'] = 3 + self.assertIn('c', asd) + asd.restore() + self.assertEqual(asd, self.default) + self.assertEqual(asd._read(), self.default) + self.assertEqual(asd._pairs, self.default) + + def test_pop(self) -> None: + asd = AutoSaveDict(self.path, **self.default) + asd.init() + + key = tuple(self.default.keys())[0] + asd.pop(key) + + self.assertNotIn(key, asd._read()) + self.assertNotIn(key, asd._pairs) + self.assertNotIn(key, asd) + + def test_popitem(self) -> None: + asd = AutoSaveDict(self.path, **self.default) + asd.init() + key = tuple(asd.keys())[-1] + val = asd[key] + result = asd.popitem() + self.assertEqual(result, (key, val)) + self.assertNotIn(key, asd) + self.assertNotIn(key, asd._read()) + self.assertNotIn(key, asd._pairs) + + def test_update(self) -> None: + asd = AutoSaveDict(self.path, **self.default) + asd.init() + new = {'b': 3} + asd.update(new) + self.assertEqual(asd['b'], new['b']) + self.assertEqual(asd._read()['b'], new['b']) + self.assertEqual(asd._pairs['b'], new['b']) + + def test_copy(self) -> None: + copy_path = 'test_copy.json' + asd = AutoSaveDict(self.path, **self.default) + asd_copy = asd.copy(copy_path) # type: ignore + asd.init() + asd_copy.init() + + self.assertEqual(asd_copy, asd) + self.assertEqual(asd_copy, asd._read()) + self.assertEqual(asd_copy, asd._pairs) + os.remove(copy_path) + + def test_fromfile(self) -> None: + file = 'test_fromfile.json' + config = {'b': 3, 'c': 4} + + with open(file, mode='w') as f: + json.dump(config, f) + os.remove(self.path) + asd = AutoSaveDict.fromfile(file, self.path) # type: ignore + asd.init() + + self.assertEqual(config, asd) + self.assertEqual(config, asd._pairs) + self.assertEqual(config, asd._read()) + self.assertIsInstance(asd, AutoSaveDict) + os.remove(file) + + def test_frommapping(self) -> None: + path = 'test_frommapping.json' + mapping = ( + ('a', 1), + ('b', 2), + ('c', 3), + ) + expected = dict(mapping) + + asd = AutoSaveDict.frommapping(mapping, path) # type: ignore + asd.init() + + self.assertEqual(expected, asd) + self.assertEqual(expected, asd._read()) + self.assertEqual(expected, asd._pairs) + os.remove(path) + + def test_fromkeys(self) -> None: + path = 'test_fromkeys.json' + keys = 'test' + expected = dict.fromkeys(keys) + + asd = AutoSaveDict.fromkeys(keys, file_path=path) # type: ignore + asd.init() + self.assertEqual(asd, expected) + self.assertEqual(asd._read(), expected) + self.assertEqual(asd._pairs, expected) + os.remove(path) + + def test_setitem(self) -> None: + expected = {**self.default, "z": 3} + asd = AutoSaveDict(self.path, **self.default) + asd['z'] = 3 + self.assertEqual(asd, expected) + self.assertEqual(asd._read(), expected) + self.assertEqual(asd._pairs, expected) + + def test_delitem(self) -> None: + config = self.default.copy() + key = tuple(config.keys())[0] + asd = AutoSaveDict(self.path, **config) + asd.init() + + del config[key] + del asd[key] + self.assertEqual(asd, config) + self.assertEqual(asd._read(), config) + self.assertEqual(asd._pairs, config) diff --git a/Bot/logger/__init__.py b/Bot/logger/__init__.py new file mode 100644 index 0000000..91743ce --- /dev/null +++ b/Bot/logger/__init__.py @@ -0,0 +1 @@ +from .logger import * # noqa diff --git a/Bot/logger/__pycache__/__init__.cpython-311.pyc b/Bot/logger/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b96903420c339c220e239637d6e27573a9cd1c27 GIT binary patch literal 200 zcmZ3^%ge<81Q`N{Q{;g3V-N=hn4pZ$d_cx@h7^Vr#vFzah7_h?22JLdj6gw6##@Y9 zen65fCqF$swP+>7XONy>Hu@R)xvBaEnfgxoCB^zKsX3`7sVM>Z#Xyd8QDRz2a87Dz zNd{0-AEHq|K0Y%qvm`!Vub}c5hfQvNN@-52T@eS+FpxdP;y~g9Gb1D84F=H*sHlhy GCg`kf((JfDRMyiF^Gc(44TX@8G%BYjJFuI z{D34|PJViNYSBuDA{L-9nD}L*pOK%Ns$Y<)pOjxxtnZSVlUkCR5|CdE1KIN#h#3HTUn+|L literal 0 HcmV?d00001 diff --git a/Bot/logger/__pycache__/logger.cpython-311.pyc b/Bot/logger/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4a7dff9e73f7c7201bb1df01c986c65f23791bd GIT binary patch literal 18888 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 literal 0 HcmV?d00001 diff --git a/Bot/logger/__pycache__/logger.cpython-39.pyc b/Bot/logger/__pycache__/logger.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..644010e850032a89bceb3e4bfa1854ca0cfc5f5d GIT binary patch literal 12178 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 literal 0 HcmV?d00001 diff --git a/Bot/logger/logger.py b/Bot/logger/logger.py new file mode 100644 index 0000000..01da3f0 --- /dev/null +++ b/Bot/logger/logger.py @@ -0,0 +1,320 @@ +from __future__ import annotations +import os +import sys +from enum import Enum +from inspect import currentframe +from typing import ( + Optional, + Tuple, + Dict, + List, + Any, +) + +__name__ = 'testing' + +__all__ = [ + 'UnhandledLogError', + 'get_color', + 'Logger', +] + + +class Color(Enum): + # Color end string, color reset + RESET = "\033[0m" + # Regular Colors. Normal color, no bold, background color etc. + BLACK = "\033[0;30m" # BLACK + RED = "\033[0;31m" # RED + GREEN = "\033[0;32m" # GREEN + YELLOW = "\033[0;33m" # YELLOW + BLUE = "\033[0;34m" # BLUE + MAGENTA = "\033[0;35m" # MAGENTA + CYAN = "\033[0;36m" # CYAN + WHITE = "\033[0;37m" # WHITE + # Bold colors + BLACK_BOLD = "\033[1;30m" # BLACK + RED_BOLD = "\033[1;31m" # RED + GREEN_BOLD = "\033[1;32m" # GREEN + YELLOW_BOLD = "\033[1;33m" # YELLOW + BLUE_BOLD = "\033[1;34m" # BLUE + MAGENTA_BOLD = "\033[1;35m" # MAGENTA + CYAN_BOLD = "\033[1;36m" # CYAN + WHITE_BOLD = "\033[1;37m" # WHITE + + +def get_color(color: str) -> str: + """Colors: + ``` + ( + "RESET", + "BLACK", + "RED", + "GREEN", + "YELLOW", + "BLUE", + "MAGENTA", + "CYAN", + "WHITE", + "BLACK_BOLD", + "RED_BOLD", + "GREEN_BOLD", + "YELLOW_BOLD", + "BLUE_BOLD", + "MAGENTA_BOLD", + "CYAN_BOLD", + "WHITE_BOLD", + ) + ``` + """ + return {i.name: i.value for i in Color}[color.upper()] + + +class Config: + _INSTANCE = 0 + + def __init__(self, level: int) -> None: + Config._INSTANCE += 1 + self._INSTANCE = Config._INSTANCE + self._settings = { + 'success': 2, + 'info': 3, + 'custom': 2, + 'warning': 1, + 'error': 1, + 'debug': 2, + } + self.level = level + self._iter = 0 + + def __str__(self) -> str: + return f"<{self.__class__.__name__}({self._settings})>" + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(settings={self._settings},\ + level={self._level} instance={self._INSTANCE})>" + + def __bool__(self) -> bool: + return bool(self._settings) + + def __getitem__(self, k: str) -> int: + return self._settings[k] + + def __int__(self) -> int: + return self.level + + def __len__(self) -> int: + return len(self._settings) + + def __contains__(self, __o: Any) -> bool: + return __o in self._settings + + def __iter__(self) -> Config: + return self + + def __next__(self) -> str: + keys = self.keys() + if self._iter >= len(self): + self._iter = 0 + raise StopIteration + retval = keys[self._iter] + self._iter += 1 + return retval + + @property + def settings(self) -> Dict[str, int]: + return self._settings + + @property + def level(self) -> int: + return self._level + + @level.setter + def level(self, v: int) -> None: + """Level setter validator + + :param v: The level to change to + :type v: int + :raises ValueError: If the v is invalid for `level` + """ + if not 0 < v <= 5: + raise ValueError(f"Level must be between `0-5` not `{v}`") + self._level = v + + def items(self): # type: ignore + yield self._settings.items() + + def keys(self) -> List[str]: + return list(self._settings.keys()) + + def values(self) -> List[int]: + return list(self._settings.values()) + + def update(self, **settings: int) -> None: + """Change the settings configuration for a given instance + + :raises ValueError: If the configuraion does not exist or\ + user tries to update to an invalid value + """ + if all(key in self.settings for key in settings) and\ + all(0 < settings[key] <= 5 for key in settings): + self._settings.update(settings) + else: + raise ValueError(f"Invalid key or value in {settings}. Remember,\ + key has to exists in {self.settings} and all values have to be between (1-5)") + + def get(self, key: str) -> int: + """Get a setting configuration + + :param key: Key of the configuration + :type key: str + :return: The level this configuration is set to + :rtype: int + """ + return self._settings[key] + + +class UnhandledLogError(Exception): ... + + +class MetaLogger(type): + """A simple metaclass that checks if all logs/settings are correctly + handled by comaring the ammount of settings in Config._settings agnainst + the functions that live in Logger() - some unnecessary + """ + def __new__(self, name: str, bases: + Tuple[type], attrs: Dict[str, Any]) -> type: + error_msg = "We either have a log function that is not in settings OR\ + a function that needs to be dissmissed OR a setting that is\ + not added as a log function" + + log_functions = 0 + target_log_functions = len(Config(1)) + dismiss_attrs = ('settings', 'get_line_info') + for log in attrs: + if not log.startswith('_') and log not in dismiss_attrs: + log_functions += 1 + if not log_functions == target_log_functions: + raise UnhandledLogError(error_msg) + return type(name, bases, attrs) + + +class Logger(metaclass=MetaLogger): + """The Logger class handles debbuging with colored information + based on the level. Each instance has its own settings which the + user can change independently through `self.settings.update()`. + Mind that level 1 will print evetything and level 5 less + """ + def __init__(self, level: int = 2, log_path: Optional[str] = None): + """Initializer of Logger object + + :param level: The level of debugging. Based on that, some informations\ + can be configured to not show up thus lowering the verbosity, defaults to 2 + :type level: int, optional + """ + self._settings = Config(level) + self._log_path = log_path + + def __str__(self) -> str: + return f"<{self.__class__.__name__}Object-{self._settings._INSTANCE}>" + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(instance={self._settings._INSTANCE})>" + + @property + def settings(self) -> Config: + """Getter so self._settings cannot be writable + + :return: Config instance + :rtype: Config + """ + return self._settings + + def _log(self, header: str, msg: str) -> None: + """If a file log_path is provided in `Logger.__init__` + + :param header: The msg header WARNING/ERROR ... + :type header: str + :param msg: The message to be logged + :type msg: str + """ + if self._log_path is not None: + if not os.path.exists(self._log_path): + with open(self._log_path, mode='w') as _: ... + with open(self._log_path, mode='a') as f: + f.write(f"[{header.upper()}]: {msg}\n") + + def _runner(self, func_name: str) -> bool: + """Use to check the `self.level` before printing the log + + :param func_name: Name of the function as a string + :type func_name: str + :return: Wheather the settings allow this certain function to print + :rtype: bool + """ + return self.settings.level <= self.settings[func_name] + + def get_line_info(self, file: str, prompt: str) -> str: + """Get the file and the line of where this function is called + + :param file: The file where the func is called (Recommended: `__file__`) + :type file: str + :param prompt: Any message to follow after line info + :type prompt: str + :return: *file path*, *line number* *prompt* + :rtype: str + """ + cf = currentframe() + msg = f"File \"{file}\", line {cf.f_back.f_lineno}" # type: ignore + self.debug(f"{msg} : {prompt}") + print(file) + return msg + + # ONLY LOGGING FUNCTIONS AFTER THIS + def custom(self, msg: str, header: str = 'custom', + *args: Any, color: str = get_color('reset'), **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{color}[{header.upper()}]: {msg}{get_color('reset')}", *args, end='', **kwargs) + print(get_color('reset')) + self._log(header, msg) + + def info(self, msg: str, *args: Any, **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{Color.YELLOW.value}[{func_name.upper()}]: {msg}", + *args, end='', **kwargs) + print(get_color('reset')) + self._log(func_name, msg) + + def success(self, msg: str, *args: Any, **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{Color.GREEN.value}[{func_name.upper()}]: {msg}", + *args, end='', **kwargs) + print(get_color('reset')) + self._log(func_name, msg) + + def warning(self, msg: str, *args: Any, **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{Color.RED.value}[{func_name.upper()}]: {msg}", + *args, end='', **kwargs) + print(get_color('reset')) + self._log(func_name, msg) + + def error(self, msg: str, *args: Any, **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{Color.RED_BOLD.value}[{func_name.upper()}]: {msg}", + *args, end='', **kwargs) + print(get_color('reset')) + self._log(func_name, msg) + + def debug(self, msg: str, *args: Any, **kwargs: Any) -> None: + func_name = sys._getframe().f_code.co_name + if self._runner(func_name): + print(f"{Color.BLUE.value}[{func_name.upper()}]: {msg}", + *args, end='', **kwargs) + print(get_color('reset')) + self._log(func_name, msg) diff --git a/Bot/logger/tests.py b/Bot/logger/tests.py new file mode 100644 index 0000000..47f224c --- /dev/null +++ b/Bot/logger/tests.py @@ -0,0 +1,123 @@ +import os +import io +import sys +import unittest +import unittest.mock +from pathlib import Path +from logger import Logger +from .logger import Config +from typing import Any + + +BASE_DIR = Path(__file__).parent + + +class TestLogger(unittest.TestCase): + def setUp(self) -> None: + self.logger = Logger(1) + + def tearDown(self) -> None: + self.logger = Logger(1) + + def test_invalid_instance(self) -> None: + with self.assertRaises(ValueError, msg='An instance with level=0 is created'): + Logger(0) + + with self.assertRaises(ValueError, msg='An instance with level=6 is created'): + Logger(6) + + logger = Logger(2) + with self.assertRaises(ValueError, msg='A settings instance changed level above limit after creation'): # noqa + logger.settings.level = 6 + + logger = Logger(2) + with self.assertRaises(ValueError, msg='A settings instance changed level above limit after creation'): # noqa + logger.settings.level = 0 + + def test_instance_counter(self) -> None: + Config._INSTANCE = 0 + l1 = Logger() + self.assertEqual(l1.settings._INSTANCE, 1, msg="First instance counter is wrong") + l2 = Logger() + self.assertEqual(l2.settings._INSTANCE, 2, msg="Second instance counter is wrong") + + self.assertEqual(l1.settings._INSTANCE, 1, msg="Repeated instance counter is wrong") + + def test_update_settings(self) -> None: + self.logger.settings.update(success=4) + self.assertEqual(self.logger.settings['success'], 4) + + with self.assertRaises(TypeError, msg="self.logger object is directly assignable"): + self.logger.settings['success'] = 4 # type: ignore + + with self.assertRaises(ValueError, msg="not existing key passed into self.settings"): + self.logger.settings.update(not_exists=4) + + with self.assertRaises(TypeError, msg="String passed as value in self.settings"): + self.logger.settings.update(success='4') # type: ignore + + with self.assertRaises(ValueError, msg='0 Passed as valid key in self.settings'): + self.logger.settings.update(success=0) + + with self.assertRaises(ValueError, msg="Above the allowed limit passed as valid key in self.settings"): # noqa + self.logger.settings.update(success=6) + + with self.assertRaises(AttributeError, msg="self.settings is overwritable (`=`)"): + self.logger.settings.settings = 'fsaf' # type: ignore + + self.logger.settings.update(success=2, info=1) + self.assertEqual(self.logger.settings['success'], 2, msg='Error in updating 2 values at once at `success`') # noqa + self.assertEqual(self.logger.settings['info'], 1, msg='Error in updating 2 values at once at `info`') # noqa + + def test_get_settings(self) -> None: + with self.assertRaises(AttributeError, msg="self.settings is overwritable (`=`) instead of read-only"): # noqa + self.logger.settings = 'fsaf' # type: ignore + + def test_get_setting(self) -> None: + self.assertEqual(self.logger.settings.get('info'), 3, msg="settings.get() returns value from wrong key or the value has changed") # noqa + with self.assertRaises(KeyError): + self.logger.settings.get('not_existing') + + @unittest.mock.patch('sys.stdout', new_callable=io.StringIO) + def test_success_msg(self, mock_stdout: Any) -> None: + msg = "anythong" + + self.logger.settings.level = 5 + self.logger.success(msg) + self.assertFalse(mock_stdout.getvalue()) + + self.logger.info(msg) + self.assertTrue(mock_stdout.getvalue()) + + self.logger.settings.level = 1 + self.logger.success(msg) + self.assertTrue(mock_stdout.getvalue()) + + self.logger.info(msg) + self.assertTrue(mock_stdout.getvalue()) + + def test_iter_next(self) -> None: + for i2 in self.logger.settings: ... + + for i1 in self.logger.settings: ... + + self.assertEqual(i1, i2, msg="There is something wrong with Config.__iter__ and Config.__next__. Maybe the index (self._iter) is not refreshing correctly") # noqa + + def test_log_to_file(self) -> None: + msg = 'This should be written in the file' + file = Path(f"{BASE_DIR}tests.txt") + logger = Logger(1, str(file)) + + # Redirect the annoying log to '/dev/null' + # (or according file for other platforms) and bring it back + std_out = sys.stdout + f = open(os.devnull, mode='w') + sys.stdout = f + logger.info(msg) + f.close() + sys.stdout = std_out + + self.assertTrue(os.path.exists(file)) + with open(file, mode='r') as f: + self.assertEqual(f"[INFO]: {msg}", f.read()[:-1]) # Slice to remove '\n' from the file + os.remove(file) diff --git a/Bot/main.py b/Bot/main.py new file mode 100644 index 0000000..28402e8 --- /dev/null +++ b/Bot/main.py @@ -0,0 +1,250 @@ +# mypy: disable-error-code=attr-defined +import os +import sys +import praw # type: ignore +import time +import utils +import prawcore # type: ignore +import traceback +import datetime as dt +from pathlib import Path +from logger import Logger +from jsonwrapper import AutoSaveDict +from typing import ( + Optional, + Callable, + Tuple, + List, + Set, + Any, +) +from bot import ( + Datatype, + Posts, + Row, +) + + +def config_app(path: Path) -> AutoSaveDict: + config = { + 'client_id': '', + 'client_secret': '', + 'user_agent': '', + 'username': '', + 'password': '', + 'sub_name': '', + 'max_days': '', + 'max_posts': '', + 'sleep_minutes': '', + } + + configuration: List[List[str]] = [] + + if not os.path.exists(path): + for key, _ in config.items(): + config_name = ' '.join(key.split('_')).title() + user_inp = input(f"{config_name}: ") + configuration.append([key, user_inp]) + + for config_name, value in configuration: + config[config_name] = value + + config_handler = AutoSaveDict( + path, + **config + ) + return config_handler + + +config_dir = Path(utils.BASE_DIR, 'config') +config_dir.mkdir(parents=True, exist_ok=True) +config_file = Path(config_dir, 'config.json') +handler = config_app(config_file) +handler.init() +posts = Posts('deleted_posts', config_dir) +logger = Logger(1) +untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED) +posts.init() +reddit = praw.Reddit( + client_id=handler['client_id'], + client_secret=handler['client_secret'], + user_agent=handler['user_agent'], + username=handler['username'], + password=handler['password'], +) + + +def remove_method(submission: praw.reddit.Submission) -> Optional[str]: + removed = submission.removed_by_category + if removed is not None: + # if removed in ('author', 'moderator'): + # method = 'Removed by moderator' + if removed in ('author',): + method = 'Deleted by OP' + elif removed in ('moderator',): + method = 'Removed by mod' + elif removed in ('deleted',): + method = 'Deleted by user' + else: + method = 'Uknown deletion method' + return method + + return None + + +def send_modmail(reddit: praw.Reddit, subreddit: str, subject: str, msg: str) -> None: + # build the payload for the compose API + # Note: The caller provides subject/msg; subreddit is used for the `to` field. + data = { + "subject": subject, + "text": msg, + "to": f"/r/{subreddit}", + } + try: + print("Sending modmail via api/compose/") + reddit.post("api/compose/", data=data) + except Exception: + # fallback/report if necessary + print("Failed to send modmail with new method") + raise + + +def notify_if_error(func: Callable[..., int]) -> Callable[..., int]: + def wrapper(*args: Any, **kwargs: Any) -> int: + try: + return func(*args, **kwargs) + except KeyboardInterrupt: + logger.debug("\nProgram interrupted by user") + return 0 + except: + author = 'https://www.reddit.com/user/kaerfkeerg' + full_error = traceback.format_exc() + bot_name = utils.BOT_NAME + msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})" + send_modmail( + reddit, + handler['sub_name'], + f'An error has occured with {utils.BOT_NAME} msg', + msg + ) + return 1 + return wrapper + + +def should_be_tracked( + flair: utils.Flair, + untracked_flairs: Tuple[utils.Flair, ...]) -> bool: + return flair not in untracked_flairs + + +def user_is_deleted(submission: praw.reddit.Submission) -> bool: + return submission.author is None + + +def check_submission(submission: praw.reddit.Submission, saved_submission_ids: Set[Row]) -> None: + if not user_is_deleted(submission) and submission.id not in saved_submission_ids: + flair = utils.get_flair(submission.link_flair_text) + method = remove_method(submission) + if should_be_tracked(flair, untracked_flairs): + if method is None and submission.author is not None: + original_post = Row( + username=submission.author.name, + title=submission.title, + text=submission.selftext, + post_id=submission.id, + deletion_method=Datatype.NULL, + post_last_edit=Datatype.NULL, + record_created=str(dt.datetime.now()), + record_edited=str(dt.datetime.now()), + ) + posts.save(original_post) + + +@notify_if_error +def main() -> int: + # run indefinitely, sleeping between iterations + while True: + posts_to_delete: Set[Row] = set() + ignore_methods = ['Removed by mod',] + + if utils.parse_cmd_line_args(sys.argv, logger, config_file, posts): + return 0 + + saved_submission_ids = {row.post_id for row in posts.fetch_all()} + max_posts = handler['max_posts'] + limit = int(max_posts) if max_posts else None + sub_name = handler['sub_name'] + + for submission in reddit.subreddit(sub_name).new(limit=limit): + try: + check_submission(submission, saved_submission_ids) + except prawcore.exceptions.TooManyRequests: + time.sleep(60) + check_submission(submission, saved_submission_ids) + + for stored_post in posts.fetch_all(): + try: + submission = reddit.submission(id=stored_post.post_id) + max_days = int(handler['max_days']) + created = utils.string_to_dt(stored_post.record_created).date() + flair = utils.get_flair(submission.link_flair_text) + + if utils.submission_is_older(created, max_days) or flair in untracked_flairs: + posts_to_delete.add(stored_post) + continue + + submission = reddit.submission(id=stored_post.post_id) + method = remove_method(submission) + if user_is_deleted(submission): + if method not in ignore_methods: + send_modmail( + reddit, + handler['sub_name'], + "User's account has been deleted", + utils.modmail_removal_notification(stored_post, 'Account has been deleted') + ) + posts_to_delete.add(stored_post) + + elif method is not None and not stored_post.deletion_method: + if method not in ignore_methods: + stored_post.deletion_method = method + stored_post.record_edited = str(dt.datetime.now()) + posts.edit(stored_post) + msg = utils.modmail_removal_notification(stored_post, method) + send_modmail( + reddit, + handler['sub_name'], + 'A post has been deleted', + msg + ) + posts_to_delete.add(stored_post) + time.sleep(utils.MSG_AWAIT_THRESHOLD) + + if submission.selftext != stored_post.text\ + or submission.selftext != stored_post.post_last_edit\ + and not stored_post.deletion_method: + stored_post.post_last_edit = submission.selftext + stored_post.record_edited = str(dt.datetime.now()) + posts.edit(stored_post) + except prawcore.exceptions.TooManyRequests: + time.sleep(60) + + for row in posts_to_delete: + posts.delete(post_id=row.post_id) + + posts_to_delete.clear() + logger.info("Program finished successfully") + logger.info(f"Total posts deleted: {len(posts_to_delete)}") + + # wait before the next cycle + sleep_minutes = int(handler['sleep_minutes']) if handler['sleep_minutes'] else 5 + time.sleep(sleep_minutes * 60) + + # end of while True + return 0 + + +if __name__ == '__main__': + sys.exit( + main() + ) \ No newline at end of file diff --git a/Bot/main.py.bkup b/Bot/main.py.bkup new file mode 100644 index 0000000..094c60e --- /dev/null +++ b/Bot/main.py.bkup @@ -0,0 +1,228 @@ +# mypy: disable-error-code=attr-defined +import os +import sys +import praw # type: ignore +import time +import utils +import prawcore # type: ignore +import traceback +import datetime as dt +from pathlib import Path +from logger import Logger +from jsonwrapper import AutoSaveDict +from typing import ( + Optional, + Callable, + Tuple, + List, + Set, + Any, +) +from bot import ( + Datatype, + Posts, + Row, +) + + +def config_app(path: Path) -> AutoSaveDict: + config = { + 'client_id': '', + 'client_secret': '', + 'user_agent': '', + 'username': '', + 'password': '', + 'sub_name': '', + 'max_days': '', + 'max_posts': '', + } + + configuration: List[List[str]] = [] + + if not os.path.exists(path): + for key, _ in config.items(): + config_name = ' '.join(key.split('_')).title() + user_inp = input(f"{config_name}: ") + configuration.append([key, user_inp]) + + for config_name, value in configuration: + config[config_name] = value + + config_handler = AutoSaveDict( + path, + **config + ) + return config_handler + + +config_file = Path(utils.BASE_DIR, 'config.json') +handler = config_app(config_file) +handler.init() +posts = Posts('post', utils.BASE_DIR) +logger = Logger(1) +untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED) +posts.init() +reddit = praw.Reddit( + client_id=handler['client_id'], + client_secret=handler['client_secret'], + user_agent=handler['user_agent'], + username=handler['username'], + password=handler['password'], +) + + +def remove_method(submission: praw.reddit.Submission) -> Optional[str]: + removed = submission.removed_by_category + if removed is not None: + # if removed in ('author', 'moderator'): + # method = 'Removed by moderator' + if removed in ('author',): + method = 'Deleted by OP' + elif removed in ('moderator',): + method = 'Removed by mod' + elif removed in ('deleted',): + method = 'Deleted by user' + else: + method = 'Uknown deletion method' + return method + + return None + + +def send_modmail(reddit: praw.Reddit, subreddit: str, subject: str, msg: str) -> None: + print("Sending modmail...") + reddit.subreddit(subreddit).message(subject, msg) + print(msg) + + +def notify_if_error(func: Callable[..., int]) -> Callable[..., int]: + def wrapper(*args: Any, **kwargs: Any) -> int: + try: + return func(*args, **kwargs) + except KeyboardInterrupt: + logger.debug("\nProgram interrupted by user") + return 0 + except: + author = 'https://www.reddit.com/user/kaerfkeerg' + full_error = traceback.format_exc() + bot_name = utils.BOT_NAME + msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})" + send_modmail( + reddit, + handler['sub_name'], + f'An error has occured with {utils.BOT_NAME} msg', + msg + ) + return 1 + return wrapper + + +def should_be_tracked( + flair: utils.Flair, + untracked_flairs: Tuple[utils.Flair, ...]) -> bool: + return flair not in untracked_flairs + + +def user_is_deleted(submission: praw.reddit.Submission) -> bool: + return submission.author is None + + +def check_submission(submission: praw.reddit.Submission, saved_submission_ids: Set[Row]) -> None: + if not user_is_deleted(submission) and submission.id not in saved_submission_ids: + flair = utils.get_flair(submission.link_flair_text) + method = remove_method(submission) + if should_be_tracked(flair, untracked_flairs): + if method is None and submission.author is not None: + original_post = Row( + username=submission.author.name, + title=submission.title, + text=submission.selftext, + post_id=submission.id, + deletion_method=Datatype.NULL, + post_last_edit=Datatype.NULL, + record_created=str(dt.datetime.now()), + record_edited=str(dt.datetime.now()), + ) + posts.save(original_post) + + +@notify_if_error +def main() -> int: + posts_to_delete: Set[Row] = set() + ignore_methods = ['Removed by mod',] + + if utils.parse_cmd_line_args(sys.argv, logger, config_file, posts): + return 0 + + saved_submission_ids = {row.post_id for row in posts.fetch_all()} + max_posts = handler['max_posts'] + limit = int(max_posts) if max_posts else None + sub_name = handler['sub_name'] + + for submission in reddit.subreddit(sub_name).new(limit=limit): + try: + check_submission(submission, saved_submission_ids) + except prawcore.exceptions.TooManyRequests: + time.sleep(60) + check_submission(submission, saved_submission_ids) + + for stored_post in posts.fetch_all(): + try: + submission = reddit.submission(id=stored_post.post_id) + max_days = int(handler['max_days']) + created = utils.string_to_dt(stored_post.record_created).date() + flair = utils.get_flair(submission.link_flair_text) + + if utils.submission_is_older(created, max_days) or flair in untracked_flairs: + posts_to_delete.add(stored_post) + continue + + submission = reddit.submission(id=stored_post.post_id) + method = remove_method(submission) + if user_is_deleted(submission): + if method not in ignore_methods: + send_modmail( + reddit, + handler['sub_name'], + "User's account has been deleted", + utils.modmail_removal_notification(stored_post, 'Account has been deleted') + ) + posts_to_delete.add(stored_post) + + elif method is not None and not stored_post.deletion_method: + if method not in ignore_methods: + stored_post.deletion_method = method + stored_post.record_edited = str(dt.datetime.now()) + posts.edit(stored_post) + msg = utils.modmail_removal_notification(stored_post, method) + send_modmail( + reddit, + handler['sub_name'], + 'A post has been deleted', + msg + ) + posts_to_delete.add(stored_post) + time.sleep(utils.MSG_AWAIT_THRESHOLD) + + if submission.selftext != stored_post.text\ + or submission.selftext != stored_post.post_last_edit\ + and not stored_post.deletion_method: + stored_post.post_last_edit = submission.selftext + stored_post.record_edited = str(dt.datetime.now()) + posts.edit(stored_post) + except prawcore.exceptions.TooManyRequests: + time.sleep(60) + + for row in posts_to_delete: + posts.delete(post_id=row.post_id) + + posts_to_delete.clear() + logger.info("Program finished successfully") + logger.info(f"Total posts deleted: {len(posts_to_delete)}") + return 0 + + +if __name__ == '__main__': + sys.exit( + main() + ) diff --git a/Bot/sqlitewrapper/__init__.py b/Bot/sqlitewrapper/__init__.py new file mode 100644 index 0000000..94cd79c --- /dev/null +++ b/Bot/sqlitewrapper/__init__.py @@ -0,0 +1 @@ +from .model import * # noqa diff --git a/Bot/sqlitewrapper/__pycache__/__init__.cpython-311.pyc b/Bot/sqlitewrapper/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9acdc387bfcaec7df7dafc69e71124d6da07bb9c GIT binary patch literal 206 zcmZ3^%ge<81Q`N{Q)GekV-N=hn4pZ$d_cx@h7^Vr#vFzah7_h?22JLdj6gw6##@Y9 zen65nH$NpcXC=dDkd|N0`WgATsrm((`cC;J#riI(IjJS7DFOM#K#p@!Vp>UXPHJgM z22fJJxG*QPB(=OKv7jKeNIyP4GcU6wK3=b&@)w5<(9F`DRJ$S$pqU`sip7D%2WCb_ P#v2SG7f?|V8&D1aS?D!6 literal 0 HcmV?d00001 diff --git a/Bot/sqlitewrapper/__pycache__/__init__.cpython-39.pyc b/Bot/sqlitewrapper/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1523d81740061d357e7b4003e97687a2ee6b7245 GIT binary patch literal 180 zcmYe~<>g`kf((JfDY8KNF^Gc(44TX@8G%BYjJFuI z{D35DZhlH?&Ps+N7N97Y_~ophk)NBYUy!MvlwVS;?~5V<(KFe7v^M^q?Q*Y78Ilw>Bq-s=4F<|$LkeT-r}$U8eE!_Y6r6IGY~TX0Gb;u AGynhq literal 0 HcmV?d00001 diff --git a/Bot/sqlitewrapper/__pycache__/model.cpython-311.pyc b/Bot/sqlitewrapper/__pycache__/model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f88815ed74258ce74371ed22c424f6f8bfa09297 GIT binary patch literal 15619 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 literal 0 HcmV?d00001 diff --git a/Bot/sqlitewrapper/__pycache__/model.cpython-39.pyc b/Bot/sqlitewrapper/__pycache__/model.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..941df627debcefa4ebbeb385f46f02ed5e43dd10 GIT binary patch literal 9901 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 d None: + for name, value in attrs.items(): + self.__dict__[name] = value + + def values(self) -> Tuple[Any, ...]: + return tuple(self.__dict__.values()) + + def keys(self) -> Tuple[Any, ...]: + return tuple(self.__dict__.keys()) + + def dict(self) -> Dict[Any, Any]: + return self.__dict__ + + def items(self) -> Any: + return self.__dict__.items() + + def __str__(self) -> str: + return f"" + + def __repr__(self) -> str: + return str(self) + + def __iter__(self) -> Row: + self.__n = 0 + return self + + def __next__(self) -> Any: + keys = tuple(self.__dict__.keys())[:-1] # Remove the `self.__n` + if self.__n == len(keys): + raise StopIteration + data = keys[self.__n] + self.__n += 1 + return data + + def __getitem__(self, __k: Any) -> Any: + return self.__dict__[__k] + + +class Datatype: + ID = 'INTEGER PRIMARY KEY' + NULL = None + INT = 'INTEGER' + REAL = 'REAL' + STR = 'TEXT' + BLOB = 'BLOB' + + +class ConnectionManager: + def __init__(self, db: str) -> None: + self.db = db + self._connection = connect(self.db) + + def __enter__(self) -> Connection: + return self._connection + + def __exit__(self, *args: Any) -> None: + self._connection.commit() + self._connection.close() + + +class Model: + def __init__(self, db_name: str, save_path: Path, **table: Any) -> None: + self.name = db_name + self.path = str(Path(f"{save_path}/.{db_name}.sqlite")) + self.table = table + self.table['id'] = Datatype.ID + + self.table_values = ' '.join( + f"{name} {datatype}," for (name, datatype) in table.items() + )[:-1] + + def __str__(self) -> str: + data = list(self.fetch_all()) + return f"{self.__class__.__name__}{data}" + + def __repr__(self) -> str: + return str(self) + + def __hash__(self) -> int: + return hash(self.path) + + def _get_conditions(self, **where: Any) -> str: + keys = tuple(where.keys()) + + condition = "" + for index, key in enumerate(keys): + condition += f"{key} = ?" + if index != len(keys) - 1: + condition += " AND " + + return condition + + def execute(self, query: str, values: Optional[Tuple[Row, ...]] = None) -> Any: + """Execute a query + + :param query: An SQL Query + :type query: str + :param values: vales to be added, if any, defaults to None + :type values: Optional[Tuple[Row, ...]], optional + :raises Exception: If tha database has not been initialized + before trying to execute any queries + :return: Whatever the query would return + :rtype: Any + """ + with ConnectionManager(self.path) as cur: + if values is None: + data = cur.execute(query) + else: + data = cur.execute(query, values) + return data.fetchall() + + def init(self) -> None: + """Create a table based on the `self.table` (**table) kwargs + provided upon initialization + """ + query = f""" + CREATE TABLE IF NOT EXISTS {self.name} ( + {self.table_values} + ) + """ + self.execute(query) + + def save(self, row: Row) -> None: + """Save a row into the db. Example: + ``` + >>> row = Row(name='Pantelis', age=13) + >>> self.save(row) + ``` + + :param row: A row object + :type row: Row + :raises ValueError: If the Row values does not match the db schema + """ + fields = self.table + # - 1 for the id field + if len(fields) - 1 != len(row.keys()): + raise ValueError(f"Row fields {row.keys()} do not much db schema\ + {tuple(self.table.keys())[:-1]}. Consider adding 'Datatype.NULL' for the missing fields") + + marks = [] + for _ in row.values(): + marks.append('?') + + query = f""" + INSERT INTO {self.name} {row.keys()} + VALUES ( + {", ".join(marks)} + ) + """ + self.execute(query, row.values()) + + def delete(self, **where: Any) -> None: + """Delete a row from the db. Example: + ``` + >>> # Query: `DELETE FROM {self.name} WHERE name = ? AND age = ?` + >>> # This will delete every row with name='John' and age=15 + >>> self.delete(name='John') + ``` + """ + values = tuple(where.values()) + condition = self._get_conditions(**where) + + query = f""" + DELETE FROM {self.name} + WHERE + {condition} + """ + self.execute(query, values) + + def edit(self, row: Row) -> None: + """After you picked and changed a row, use this instead of `save` in order + for the entry to preserver the same `id`. Example: + ``` + >>> row = self.get(name='john') + >>> row.name = 'Mary' + >>> self.edit(row) + ``` + + :param row: _description_ + :type row: Row + """ + id = row.id + self.delete(id=id) + + marks = [] + for _ in row.values(): + marks.append('?') + + query = f""" + INSERT INTO {self.name} {row.keys()} + VALUES ( + {", ".join(marks)} + ) + """ + + self.execute(query, row.values()) + + def _entries_as_rows(self, data: List[Any]) -> List[Row]: + """Take a list of entries and convert it to a list of `Row`s + + :param data: The list of entries + :type data: List[Any] + :return: A copy of the data as list or `Row`s + :rtype: List[Row] + """ + # rows = [ + # , + # , + # ] + table_keys = tuple(self.table.keys()) + rows = [] + + for row in data: + struct = {} + for index, col in enumerate(row): + struct[table_keys[index]] = col + rows.append(Row(**struct)) + struct.clear() + + return rows + + def fetch_all(self) -> Generator[Row, None, None]: + query = f"SELECT * FROM {self.name}" + data = self.execute(query) + + rows = self._entries_as_rows(data) + yield from rows + + def filter(self, **where: Any) -> Generator[Row, None, None]: + """Filter out data from the db based on the `where` conditions. Example + ``` + >>> data = self.filter(name='Pantelis', age=13) + >>> # Query created + >>> # SELECT * FROM test WHERE name = Pantelis AND age = 13 + >>> for i in data: + ... i + + ``` + + :yield: Row + :rtype: Generator[Row, None, None] + """ + # cursor.execute("SELECT * FROM my_table WHERE name = ? AND age = ?", (name, age)) + values = tuple(where.values()) + condition = self._get_conditions(**where) + + query = f""" + SELECT * FROM {self.name} + WHERE + {condition} + """ + + data = self.execute(query, values) + rows = self._entries_as_rows(data) + yield from rows + + def get(self, **where: Any) -> Row: + """Find the first occurance matching the `where` condition(s) Example: + ``` + >>> self.get(name="Pantelis", age=12) + + ``` + + :return: A `Row` with the values of the matching row + :rtype: Row + """ + values = tuple(where.values()) + + condition = self._get_conditions(**where) + + query = f""" + SELECT * FROM {self.name} + WHERE + {condition} + """ + data = self.execute(query, values)[0] + row = {} + for value, name in zip(data, tuple(self.table.keys())): + row[name] = value + return Row(**row) + + +if __name__ == '__main__': + pass diff --git a/Bot/sqlitewrapper/tests.py b/Bot/sqlitewrapper/tests.py new file mode 100644 index 0000000..175c730 --- /dev/null +++ b/Bot/sqlitewrapper/tests.py @@ -0,0 +1,83 @@ +# mypy: disable-error-code=attr-defined +import unittest +import os +from pathlib import Path +from .model import ( + Row, + Model, + Datatype +) + + +class TestRow(unittest.TestCase): + def setUp(self) -> None: + return super().setUp() + + def tearDown(self) -> None: + return super().tearDown() + + def test_init(self) -> None: + name, age = 'Mary', 14 + row = Row(name=name, age=age) + result = row.values() + self.assertEqual(result, (name, age)) + + +class TestModel(unittest.TestCase): + def setUp(self) -> None: + self.base_dir = Path(__file__).parent + self.name = 'testdb' + self.db = Model( + self.name, + self.base_dir, + name=Datatype.STR, + age=Datatype.INT, + ) + self.db.init() + return super().setUp() + + def tearDown(self) -> None: + os.remove(self.db.path) + return super().tearDown() + + def test_init(self) -> None: + self.assertTrue(os.path.exists(self.db.path)) + + def test_save(self) -> None: + name, age = 'John', 14 + self.db.save(Row(name=name, age=age)) + self.db.get(name=name, age=age) # This must not raise an Exception + + with self.assertRaises(ValueError): + self.db.save(Row(name='test')) + + def test_delete(self) -> None: + name, age = 'John', 14 + self.db.save(Row(name=name, age=age)) + self.db.delete(name=name, age=age) + self.assertEqual(len(tuple(self.db.fetch_all())), 0) + + def test_edit(self) -> None: + name, age = 'John', 14 + self.db.save(Row(name=name, age=age)) + r = self.db.get(name=name, age=age) + r.name = 'Mary' + self.db.edit(r) + self.assertEqual(len(tuple(self.db.fetch_all())), 1) + self.db.get(name='Mary', age=age) # This should not raise an exception + + def test_filter(self) -> None: + data = ( + ('John', 14), + ('Mary', 14), + ('Mary', 15), + ) + + for i in data: + self.db.save(Row(name=i[0], age=i[1])) + + age = 14 + filtered = self.db.filter(age=age) + data = list(filtered) # type: ignore + self.assertTrue(all(i.age == age for i in data)) # type: ignore + self.assertEqual(len(data), 2) diff --git a/Bot/utils/__init__.py b/Bot/utils/__init__.py new file mode 100644 index 0000000..49b8741 --- /dev/null +++ b/Bot/utils/__init__.py @@ -0,0 +1,2 @@ +from .constants import * # noqa +from .actions import * # noqa diff --git a/Bot/utils/__pycache__/__init__.cpython-311.pyc b/Bot/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f734f85da0c5f592edac4480365ac963687e1b74 GIT binary patch literal 231 zcmZ3^%ge<81Q`N{QyhTwV-N=hn4pZ$VnD`ph7^Vr#vFza2+atjnSvQKnO`yjB{dmu zF>3i~GTq`#&d)0@Nz5xLzQvxHT#^aotz`HNGUu1Ienx(7s(wMHzEgflvA#=cPHIVN zNI&){l?R%*!l^kJl@x{Ka9Do1apelWJGQ2{a32 dWwA1l_`uA_$asT6;sOlaU=X{2ii+5Q>Hq|hJm~-c literal 0 HcmV?d00001 diff --git a/Bot/utils/__pycache__/__init__.cpython-39.pyc b/Bot/utils/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62e44188e69dc41369390a759766a11ab29e72ac GIT binary patch literal 195 zcmYe~<>g`kf((JfDGosTF^GcKT~c#W zOHxw;@{55S=c2^4lHi=w(vl3P{1W}rlFXc9{rLFIyv&mLc)fzkTO2mI`6;D2sdgYM Ki$QMVU<3dvRxm99 literal 0 HcmV?d00001 diff --git a/Bot/utils/__pycache__/actions.cpython-311.pyc b/Bot/utils/__pycache__/actions.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1235b874e5f51b930d264b70e8b019cb2c3ad05 GIT binary patch literal 4357 zcmb6cOKcm*b(XvQXh}+>D9Mui(fT9Rv}`eU92Aw*I{H|3V23gyJBeu+>lJ4xt+nJb zv&$$FB!mr&!U+si4FX6&Q$UBpb>SR(^wCZ)J@h~c1VSue)Id+Uv2Y6rIrYsfNl~(E zB*WP^Z|2Rsd2eRkhkp)-WdtR9{_*^F3qt>5oqFLE=)CX<2(2TGuwbB++X^WU^gV`{ zl~R(x#ze!L^`(3qmkfV4kP2|zYqVtLl+1CT5zK~CA&&cvaJDto3V0yZ=0U$kxaBUw zG6_5o5qbolTT&4mBvBk9k&J}HccoN2ZY8a_jgLh$J{*Cu7>*K|M0@Is5N;n$BX?&~rr9=$z%mtio(ow=9^T>XvF6m{2EV z*;LQXskW(NJ6)5TH+3yu^g?$X5tI@zf_oEEBKD*tET+6z0=>KxZXP+l8PoWfV5enl zR?A^C2dMA*Le5;wB|(9Js^+vTQB_A)RS>;kFg&EH9~CsCM)A!|UwQxHByboXOHNKF z0rg$KoSgn3`4{wpiS)yT#Jrg$3CqaLTQ*LNo3@phBnGhw=0}>CpjyVBF-XCl2V$aN z>xPxk((FvEq5Kk;lmm8&)?om}#;+3YEKXG-2aE4;tTdd3bD@WTVzIpxdJrIGHpl@dybZk~s(M6k-?O%kwdb)c2<$#?b`*6{Nd-H-v6`|IO5i-2orX8cB~LE`K&dHe&2IO{6;t5-0}zT0?=<95A;RoH`YU6f_}rEq3?x0J91v2 z@7p)u|Hm-A%0P1Y$e5A`LsRB8OPM7kr*KmtSeB=+4lB2p<=e7+O}7m)tSk@8GJ8SF zJCno8ylv;L;Y0!qWQY=sb$cjnW)tvbNe*7zazvIVHJd1DN}$28ytOuxF$+1?pOocs zEvH-~*}MT$MieNr%;3%1d*!0tI$7WAmHYfNKExL?zzS!;*Mr3>z+cN*iv*T(W~Y@! z9S*->D_M;$uo=3oSllR2E4rm*Yl zyWmsO%+xLsBcE8*7xV<@7*}jlu?SK0jIv}FenyEx41G?YH3*|<`8?4K#hwRgz)1|n zy-uKD5z6e}@!ISv96!4%6qEwOT-yV}w;7D9t>l~0XU!%brm6zMIuovcQi z#yU`IcRAc&?Q9x5gwBqX-x#kRZW?oULhasFfx8n&;?CG-a2M|c(`GKC(5l=m{_ zt#spcgY*pN*7J9;5oSHB((Bv{_AKz#^dBo=Sxpg5T;MiEyHex*Y-K z9`55I)p(46i}N-XHd*J2=OrG$1$P(NoeS=+TRPQ1La8F_3e|!SCi)L z|0|7n$5-dQpKosXC&}gCzaHf3X~^}XPZS4cAWs4BBqx`5CcvLSq*PddR_4Kwlsq-F zd7E2#jR?*N2z+D=Lin(9^H$9Xq^X`~VQpBs3h~>vz^e9g*~^9!3sML7t-YSzdu%4> zHf!3<#bI!Ix@K6e|7r;4a>TPgmK)Y=ZC100{5^Yqj>HsmLd|k5A55BxD`J82JVs$w zp>U__1nL_uM`2o>n@hYl756)$L2{0y=Q5__F)aroH6%vFiB18Jq-jqv*UaOj<0ULy zI#QlxUtW3=Z_U53@JAW=1$sAj+nR9y=WChm;Gt6RP$ks11AP+# z@o#t9 zN;HOEnzQOzfuv+vMrc)B5qFb~6`|%_AqfUU_JARaNqBo+$AfJrz{Iz82xTH^!lq0drw&W=Rje-~vI?)dDbJmgH2MGV&&_}OsS z`pAHMW0A7lg;EyD9KWh+hM}r%0xp8=Zl2Au4o8|bO@liKdV%#taL)V&xek;?WcObM z)8vQ1sQX{&S9BV>%rROI0fLbU!gJJ8?haN^cWG~Z)tmz*w5L{3Pib$h zpu?rTwd(T+Jw<=DLlnBI=w)zk^51OqKf9!mB=oJ#R1q{A*Lkxaw8n&0;UP?6?c?Ka z`=r*oy*=>s5h1#PH{bkPuOdMI3W7lM1oNge^P$_`(s;Wcj0;{N`mp_*162gb;|bn8 dndHrW5D|on!bTcAAhesae_C*{*F%1j{{t%}Cw~9{ literal 0 HcmV?d00001 diff --git a/Bot/utils/__pycache__/actions.cpython-39.pyc b/Bot/utils/__pycache__/actions.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8b3798d159baec5ff5b2d279d72539993031a66 GIT binary patch literal 2552 zcma)8TW=f372eD2a`+O~45T&p^12Xd62`M}{TJpSC_v4|e|X;-X^E?<4@@it%koTX<) z2bOi-dF=BuybH@Y5sURrN3=yKnAqU!j{|;|pF_`i;T>NP7h&7L==1ynY?mHA*W8>{hOisJ&iKD{5~PN}~;LG>3;mLh?Us9`r&J z?3Z~V&B~$B+1QG&Rt>N6qRgbIn$Pkws~cU6i&3tNrZ(p$xl|$>RXi(;T4cFAROXzT z4y!^b49E(VH6<6)w3U`ceVFMc<9b9Mp8i}Y@1dwmsDzWc+`(D6sn5NXaX$?>Lu7%( zc{*u$(3GDEp04Z;^O`rc;3oX&Ufn#X2M8g`vO2FsmYF!q;QX{Cxt(SAr+K;T2@h`G z`1tysS=oJmcd&PJaD6WgKl;t!=AFUc9l3!$Z1wF<|EQ@%e^T^^jaL0VQ3@@%O=|y| z%*Xnm6jOclexv(SU6iVyk0>i@dvanuy+9EW+#;9ZXKg*t1dGguQs}9yM-=!S71dT804v;dlqHbl$5aQ@te+pWRuXS>M+NKKA%l?;Wb6N@#$?)7CDp^+;pTEHIiq`*=B923+yL2lD&zr^l~+Z&~Dh!(#Sd)@J{aG!8B>dw0Up-bk}LYU*)un2ifK z6Oc1%k$y&tEx!Qmk7)T43U?o}1zxy!kI8?|aS)q0gnOQRYwkXA=j^HLI`toyb_V-! z*3sS*f9_Gw9q0bvfHHRS=>lb#6Yzj~?xN?aQ|~WlhM4)6S=Y|=kHZ({{^``*)~UJi za}RiQ0B*$L$Aj57*P5!rJx+)~k|R-0l8J1piH;L|%TAm&I;>?E6tk0j@_Vc7iIFTO zbm?}ITi_S15`FaIDlaZ0g#xuLlKcOc`;bQ3)o}R@3xoey&* z;;-ql8%BYp?bCkEUKupW!o{hyJev><7I00Y#ZW$+tz+1;)fSz%_j@hlm!dYTsK<@* z8f8E;AZ}sS_p!#H(fb5?OA96t#14i{=yeEW0)OOdRJ~3WVK>&XP(na2H%B=s{9X-PL}XK%`OCW(k;8Z1wn&T zo_J54C;n6Cp*IJ|Tyh@zbN?9Z0=U0P&mZ>yD}R7Y1jggqM6`=8aI<~95Qf=JQJ<(D zlUJ~)+(wnQM^hmdEgrnZ zt$mr}DKr}vG4>)X+${GgIqwZ-8(Vj;Y*kmbc(V1Yovq*QY#nTGjeEZAW3;@A%0yTl zxUU4b{T;HtL_TY<{SHOhgu0US-epNyvPKTVEX z>;*uBax#;OKu*8K9v`2WlM^3b1d_hR5g(tHnUfkHe~Yaku_!gKq=*G5T*L+>Rx*4B zk_^9W^fU5vQ}qin^?`;K>${}pq?V+n1mqV3IjA=3mzHGa6zeDF=M|SE=9Lud6;%G> zu*uC&Da}c>D-r-21`3X1cOdbBnURt427|-}RP=$3g`25?{R1BZk4yu{4Q~DplL;K1 dc0G1ixD~E&t6$;P1B!}?Utkc1pdvn?SpZ0SY?%N6 literal 0 HcmV?d00001 diff --git a/Bot/utils/__pycache__/constants.cpython-39.pyc b/Bot/utils/__pycache__/constants.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d3f1acc412dc5ef776e47f55b67f6166f21d50d GIT binary patch literal 327 zcmY*TOHRWu5ViAZBA^u#rw9vtfeMk5h*qK>HIb0Av0UsbLC#0EvtZ3>I7e4p!CO|n z0xQNln9;nMH=21fY&yL_VDd!iET81kZcahy<{UwnUs{Oz_FOpV K$`7Id&+sqTrBo>Z literal 0 HcmV?d00001 diff --git a/Bot/utils/actions.py b/Bot/utils/actions.py new file mode 100644 index 0000000..ab1ce5e --- /dev/null +++ b/Bot/utils/actions.py @@ -0,0 +1,99 @@ +# mypy: disable-error-code=attr-defined +import os +import datetime as dt +from bot import Posts +from pathlib import Path +from enum import Enum +from typing import List +from logger import Logger +from sqlitewrapper import Row + + +__all__ = ( + 'Flair', + 'get_flair', + 'modmail_removal_notification', + 'parse_cmd_line_args', + 'submission_is_older', + 'string_to_dt', +) + + +class Flair(Enum): + SOLVED = 'Solved' + ABANDONED = 'Abandoned' + UKNOWN = 'Uknown' + + +def get_flair(flair: str) -> Flair: + try: + return Flair(flair) + except ValueError: + return Flair('Uknown') + + +def modmail_removal_notification(submission: Row, method: str) -> str: + return f"""A post has been removed + +OP: `{submission.username}` + +Title: {submission.title} + +Post ID: https://old.reddit.com/comments/{submission.post_id} + +Date created: {submission.record_created} + +Date found: {submission.record_edited} + +Ban Template; + + [Deleted post](https://reddit.com/comments/{submission.post_id}). + + Deleting an answered post, without marking it solved, is against our rules. + + You can read [our rules](https://reddit.com/r/MinecraftHelp/wiki/rules) to see if you're eligible to appeal this ban.""" + + +def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, posts: Posts) -> bool: + help_msg = """Command line help prompt + Command: help + Args: [] + Decription: Prints the help prompt + + Command: reset_config + Args: [] + Decription: Reset the bot credentials + + Command: reset_db + Args: [] + Decription: Reset the database +""" + if len(args) > 1: + if args[1] == 'help': + logger.info(help_msg) + elif args[1] == 'reset_config': + try: + os.remove(config_file) + except FileNotFoundError: + logger.error("No configuration file found") + elif args[1] == 'reset_db': + try: + os.remove(posts.path) + except FileNotFoundError: + logger.error("No database found") + else: + logger.info(help_msg) + return True + return False + + +def submission_is_older(submission_date: dt.date, max_days: int) -> bool: + current_date = dt.datetime.now().date() + time_difference = current_date - submission_date + if time_difference.days > max_days: + return True + return False + + +def string_to_dt(date_string: str) -> dt.datetime: + return dt.datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') diff --git a/Bot/utils/constants.py b/Bot/utils/constants.py new file mode 100644 index 0000000..e8ac1e6 --- /dev/null +++ b/Bot/utils/constants.py @@ -0,0 +1,13 @@ +from pathlib import Path + + +__all__ = ( + 'BASE_DIR', + 'BOT_NAME', + 'MSG_AWAIT_THRESHOLD', +) + + +BOT_NAME = 'CraftSleuthBot' +BASE_DIR = Path(__file__).parent.parent.parent +MSG_AWAIT_THRESHOLD = 5 diff --git a/Bot/utils/tests.py b/Bot/utils/tests.py new file mode 100644 index 0000000..14213c0 --- /dev/null +++ b/Bot/utils/tests.py @@ -0,0 +1,48 @@ +import unittest +import datetime as dt +from .actions import ( + Flair, + get_flair, + string_to_dt, + submission_is_older, +) + + +class TestActions(unittest.TestCase): + def setUp(self) -> None: + return super().setUp() + + def tearDown(self) -> None: + return super().tearDown() + + def test_get_flair(self) -> None: + solved = get_flair("Solved") + self.assertEqual(solved, Flair.SOLVED) + abandoned = get_flair('Abandoned') + self.assertEqual(abandoned, Flair.ABANDONED) + uknown = get_flair('Uknown') + self.assertEqual(uknown, Flair.UKNOWN) + uknown = get_flair('fsdafsd') + self.assertEqual(uknown, Flair.UKNOWN) + + def test_string_to_dt(self) -> None: + datetime = dt.datetime.now() + string_dt = str(datetime) + back_to_dt = string_to_dt(string_dt) + self.assertEqual(datetime, back_to_dt) + + def test_submission_is_older(self) -> None: + max_days = 7 + today = dt.datetime.now() + + post_made = today - dt.timedelta(days=3) + result = submission_is_older(post_made.date(), max_days) + self.assertFalse(result) + + post_made = today - dt.timedelta(days=max_days) + result = submission_is_older(post_made.date(), max_days) + self.assertFalse(result) + + post_made = today - dt.timedelta(days=(max_days + 1)) + result = submission_is_older(post_made.date(), max_days) + self.assertTrue(result) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa81d91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.14-slim + +WORKDIR /app + +# Copy application files +COPY Bot ./Bot + +RUN mkdir -p /app/config + +# Install dependencies +RUN pip install --no-cache-dir praw + +ENV PYTHONUNBUFFERED=1 + +# Run the script +CMD ["python", "Bot/main.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c2b4cce --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 hor00s + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..997cb63 --- /dev/null +++ b/config/config.json @@ -0,0 +1,11 @@ +{ + "client_id": "", + "client_secret": "", + "user_agent": "", + "username": "", + "password": "", + "sub_name": "", + "max_days": "180", + "max_posts": "180", + "sleep_minutes": "5" +} \ No newline at end of file