From 8090033f6f867ded5977e4f455e89d44b1b2145c Mon Sep 17 00:00:00 2001 From: philoctetes409bc Date: Wed, 30 Dec 2020 02:52:35 -0500 Subject: [PATCH] first commit --- .gitignore | 4 + Pipfile | 25 + Pipfile.lock | 512 ++++++++++++ README.md | 111 +++ conf.example.yml | 108 +++ dev.md | 288 +++++++ mcmxi/__init__.py | 1 + mcmxi/cards.py | 403 ++++++++++ mcmxi/expression_evaluators.py | 39 + mcmxi/http_requests.py | 245 ++++++ mcmxi/irc_text_formatting.py | 215 +++++ mcmxi/mcmxi.py | 295 +++++++ mcmxi/objects/__init__.py | 18 + mcmxi/objects/bot_config.py | 41 + mcmxi/objects/bot_event.py | 1384 ++++++++++++++++++++++++++++++++ mcmxi/objects/bot_state.py | 101 +++ mcmxi/objects/channel.py | 40 + mcmxi/objects/hostmask.py | 8 + mcmxi/objects/network.py | 44 + mcmxi/objects/owner.py | 17 + mcmxi/objects/server.py | 23 + mcmxi/objects/user.py | 17 + mcmxi/term_bin.py | 43 + mcmxi/utilities.py | 242 ++++++ 24 files changed, 4224 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 conf.example.yml create mode 100644 dev.md create mode 100644 mcmxi/__init__.py create mode 100644 mcmxi/cards.py create mode 100644 mcmxi/expression_evaluators.py create mode 100644 mcmxi/http_requests.py create mode 100644 mcmxi/irc_text_formatting.py create mode 100644 mcmxi/mcmxi.py create mode 100644 mcmxi/objects/__init__.py create mode 100644 mcmxi/objects/bot_config.py create mode 100644 mcmxi/objects/bot_event.py create mode 100644 mcmxi/objects/bot_state.py create mode 100644 mcmxi/objects/channel.py create mode 100644 mcmxi/objects/hostmask.py create mode 100644 mcmxi/objects/network.py create mode 100644 mcmxi/objects/owner.py create mode 100644 mcmxi/objects/server.py create mode 100644 mcmxi/objects/user.py create mode 100644 mcmxi/term_bin.py create mode 100644 mcmxi/utilities.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d84ed42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.#* +conf.yml +BotDB.json +*.pyc \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..39ee5c2 --- /dev/null +++ b/Pipfile @@ -0,0 +1,25 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +irc = "*" +ipython = "*" +pyyaml = "*" +emoji = "*" +beautifulsoup4 = "*" +dnspython = "*" +py-expression-eval = "*" +pyroute2 = "*" +numexpr = "*" +brainfuck-interpreter = "*" +nltk = "*" +tinydb = "*" +feedparser = "*" +requests = "*" + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..3926280 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,512 @@ +{ + "_meta": { + "hash": { + "sha256": "ab5714630e56605de2816326ee14b4d91085312571d4992a97d243fb04a0547e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35", + "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25", + "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666" + ], + "index": "pypi", + "version": "==4.9.3" + }, + "brainfuck-interpreter": { + "hashes": [ + "sha256:6059fef0f12628ccd8e2fa66e8b8947af56d79373ae7f0a8a2cd89ae05c2f317" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "certifi": { + "hashes": [ + "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", + "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + ], + "version": "==2020.11.8" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, + "dnspython": { + "hashes": [ + "sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7", + "sha256:40bb3c24b9d4ec12500f0124288a65df232a3aa749bb0c39734b782873a2544d" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "emoji": { + "hashes": [ + "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" + ], + "index": "pypi", + "version": "==0.6.0" + }, + "feedparser": { + "hashes": [ + "sha256:1b00a105425f492f3954fd346e5b524ca9cef3a4bbf95b8809470e9857aa1074", + "sha256:f596c4b34fb3e2dc7e6ac3a8191603841e8d5d267210064e94d4238737452ddd" + ], + "index": "pypi", + "version": "==6.0.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "importlib-metadata": { + "hashes": [ + "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175", + "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099" + ], + "markers": "python_version < '3.8'", + "version": "==3.1.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592", + "sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5" + ], + "markers": "python_version < '3.7'", + "version": "==3.3.0" + }, + "ipython": { + "hashes": [ + "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64", + "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf" + ], + "index": "pypi", + "version": "==7.16.1" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "irc": { + "hashes": [ + "sha256:99fd5d1fa1d054dee4fbb81e0d5193dc1e8200db751d5da9a97850a62162b9ab", + "sha256:b5179428448947d364edcdcef8cb34a2a7033b4e78e58fec32d092091eab786f" + ], + "index": "pypi", + "version": "==19.0.1" + }, + "jaraco.classes": { + "hashes": [ + "sha256:116429c2047953f525afdcae165475c4589c7b14870e78b2d068ecb01018827e", + "sha256:c38698ff8ef932eb33d91c0e8fc192ad7c44ecee03f7f585afd4f35aeaef7aab" + ], + "markers": "python_version >= '3.6'", + "version": "==3.1.0" + }, + "jaraco.collections": { + "hashes": [ + "sha256:a7889f28c80c4875bd6256d9924e8526dacfef22cd7b80ff8469b4d312f9f144", + "sha256:be570ef4f2e7290b757449395238fa63d70a9255574624e73c5ff9f1ee554721" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:9fedc4be3117512ca3e03e1b2ffa7a6a6ffa589bfb7d02bfb324e55d493b94f4", + "sha256:d3dc9f6c1a1d45d7f59682a3bf77aceb685c1a60891606c7e4161e72ecc399ad" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "jaraco.logging": { + "hashes": [ + "sha256:31716fe84d3d5df39d95572942513bd4bf8ae0a478f64031eff4c2ea9e83434e", + "sha256:b05ed07101883997a30e05c2472798d86129803d9961a0d1081a3236ad37c52a" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.0" + }, + "jaraco.stream": { + "hashes": [ + "sha256:287e1cba9f278e0146fdded6bc40518930813a5584579769aeaa1d0bfd178a73", + "sha256:a42357141288bbd55938b9ff464173c078038374ce5ffa1bf31895138acc0f30" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.0" + }, + "jaraco.text": { + "hashes": [ + "sha256:c87569c9afae14f71b2e1c57f316770ab6981ab675d9c602be1c7981161bacdd", + "sha256:e5078b1126cc0f166c7859aa75103a56c0d0f39ebcafc21695615472e0f810ec" + ], + "markers": "python_version >= '2.7'", + "version": "==3.2.0" + }, + "jedi": { + "hashes": [ + "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20", + "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.2" + }, + "joblib": { + "hashes": [ + "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72", + "sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5" + ], + "markers": "python_version >= '3.6'", + "version": "==0.17.0" + }, + "more-itertools": { + "hashes": [ + "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330", + "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf" + ], + "markers": "python_version >= '3.5'", + "version": "==8.6.0" + }, + "nltk": { + "hashes": [ + "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35" + ], + "index": "pypi", + "version": "==3.5" + }, + "numexpr": { + "hashes": [ + "sha256:0eb8d1a949dcc3eea633438af939f406aaaf240ae69b4ab85ed0c11b8d5e77ee", + "sha256:208597cacb191fe983b4ae05dc9ae8177b17d82a0d9f34719d71ac614744f53b", + "sha256:280c316d56903d20a474c5e03c073371b8879842b8070606cef0c1ea7371933a", + "sha256:2cb778a74f315aafc8eded19781e444269bd45f4ce3095697595e5000dc20f8a", + "sha256:33a610bb775a84ab8ded0af4041df2e931ce7edf5b465ccd9851511429c86d0f", + "sha256:3d83f6f3d6d449eb82a4a5bd56b9d61c9e1ade65b1188052700171051329888b", + "sha256:4655276892b5274015377a4487e1c57cc257c666e5578e12679029cc1124fb08", + "sha256:49f835568c864b444fa6fccf64cc01ff51a6171311742451ac4a176df471f9d8", + "sha256:57b7fcf2d0a1370bc9a380f3a96f6d10e4dfab5081b61a198a8d23b80c33e634", + "sha256:57d9ccd0820b7f5b1bed5100dd54a5ae52c39eb5b7e54317ae29e31ed9bd9edf", + "sha256:583fcf614521edf6eb1326e982d6fe3951dbd451d63e51f7438f0142b491d43f", + "sha256:59984617a50369670a88a0f0b6decdf59a93828dc42e29c8851bcffcedf0695b", + "sha256:659cee220ebe4bf88cb527ca9723d7cb390e93cbae8729ff5e927d06713bad26", + "sha256:687fa9521dbafb130f42d61462f968f211f7eb364f2789c5fbe65d82809ad6b2", + "sha256:78c7040baf20036f0d85308fd5f8322e30d553b8daff1de264394014feb62cc0", + "sha256:7cd5369c2f8cb4bac57571e52bca1a9ccc0260567cefa39ac40680dad0e9df4c", + "sha256:81ff83abc969288673ad37055fef3e5e80cdc87f90245b76c0af9bdef6d5c509", + "sha256:841c23811b00f35b4ce2c330b57c4398ff4a61af4488ce0e013e5039bba68188", + "sha256:84d10e27833a5be6c9a61350cba2acb2f36af1e71c4d47c390b4cc80704ccb55", + "sha256:9e7dbf2a849c34f5e61f9b8119688108f7b5dec97ee8ea2946440bc69a4b28d0", + "sha256:9f91ea6385f743d5ef5ef0a074270a057115d8a4c57625800dd25b5912f563b2", + "sha256:a478e224a23609e1bef45b44a65aad2f158a3072947fc0085c231953b1fafdcd", + "sha256:b0d239d9827e1bcee08344fd05835823bc60aff97232e35a928214d03ff802b1", + "sha256:c169e1424d495b7efefe69c046cbf89ae0dc7a071a89b6b844ae328ac48fccbc", + "sha256:e518918a077478523d89060a8eb59178fd80f7f1273fe1a74088c46163fa49b5", + "sha256:e6a7d0c269a3d9e117072551e78ec5332ece7297f80acf6447d701de0328e7df", + "sha256:eb2bd8656ee2a92b2e928904d6b7ad434f559b1f74a381ff5f36ad987badd1a6" + ], + "index": "pypi", + "version": "==2.7.1" + }, + "numpy": { + "hashes": [ + "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db", + "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce", + "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1", + "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512", + "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2", + "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757", + "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9", + "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2", + "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08", + "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b", + "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb", + "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc", + "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac", + "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83", + "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36", + "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387", + "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f", + "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad", + "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c", + "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414", + "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37", + "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764", + "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753", + "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909", + "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6", + "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63", + "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9", + "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949", + "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab", + "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c", + "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3", + "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893", + "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15", + "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.19.4" + }, + "parso": { + "hashes": [ + "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", + "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.7.1" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c", + "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.8" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "py-expression-eval": { + "hashes": [ + "sha256:43038326b686df697f9533895184c15d18769e215abbd8bcecaea607483f35b3", + "sha256:fcbd26d015568752e1f41b263c722acbcc06b55ef2b4a8d0af5aabbc6b4ba16f" + ], + "index": "pypi", + "version": "==0.3.10" + }, + "pygments": { + "hashes": [ + "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", + "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" + ], + "markers": "python_version >= '3.5'", + "version": "==2.7.2" + }, + "pyroute2": { + "hashes": [ + "sha256:774c5ecf05fe40f0f601a7ab33c19ca0b24f00bf4a094e58deaa5333b7ca49b5" + ], + "index": "pypi", + "version": "==0.5.14" + }, + "pytz": { + "hashes": [ + "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", + "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" + ], + "version": "==2020.4" + }, + "pyyaml": { + "hashes": [ + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "index": "pypi", + "version": "==5.3.1" + }, + "regex": { + "hashes": [ + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + ], + "version": "==2020.11.13" + }, + "requests": { + "hashes": [ + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" + ], + "index": "pypi", + "version": "==2.25.0" + }, + "sgmllib3k": { + "hashes": [ + "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9" + ], + "version": "==1.0.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "soupsieve": { + "hashes": [ + "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55", + "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232" + ], + "markers": "python_version >= '3.0'", + "version": "==2.0.1" + }, + "tempora": { + "hashes": [ + "sha256:9af06854fafb26d3d40d3dd6402e8baefaf57f90e48fdc9a94f6b22827a60fb3", + "sha256:e319840007c2913bf2f14ebf1f71b94812335d1ef4ca178e1c65c445a2d63da8" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "tinydb": { + "hashes": [ + "sha256:1d102d06f9bb22d739d8061b490c64d420de70dca5f95ebd43a492c43c7bd303", + "sha256:c8a8887269927e077f3aa16fddbf4debd176c10edc4ac8a5ce48ced0b10adf8c" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "tqdm": { + "hashes": [ + "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22", + "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.54.0" + }, + "traitlets": { + "hashes": [ + "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", + "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" + ], + "version": "==4.3.3" + }, + "urllib3": { + "hashes": [ + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.2" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + }, + "zipp": { + "hashes": [ + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" + ], + "markers": "python_version < '3.8'", + "version": "==3.4.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c17a83 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# Quickstart + +- This bot uses notices to send information that is personalized or private to you, use one of the following two +sections (irssi or weechat) + +## Irssi +- `/WINDOW LEVEL +NOTICES` + +## Weechat +- `/set irc.msgbuffer.notice = current` + +## Bot +- `pipenv` is required, install it with `pip install pipenv` or your OS package manager. +- Clone this repository +- copy the configuration example, to `config.yml` and edit it. Not much is required to start, just the obvious settiings. +- run `pipenv shell` +- run `pipenv sync` +- run `python -m mcmxi.mcmxi` + +### Console +- the bot uses IPython embedded for control and development + +``` +❯ python -m mcmxi.mcmxi +INFO|mcmxi-> bot is initialized, press C-d at any point for a smooth and easy exit, starting REPL... +Python 3.6.10 (default, Jan 16 2020, 09:12:04) [GCC] +Type 'copyright', 'credits' or 'license' for more information +IPython 7.16.1 -- An enhanced Interactive Python. Type '?' for help. + +... + +``` + +### Extensibility +- To extend the bot, create a script to the following effect: + +``` +from mcmxi import mcmxi +if __name__ == "__main__": + l = UTIL.setup_logging(bot_config()) + + bots = mcmxi.start_bots() + + l.info("bot is initialized, press C-d at any point for a smooth and easy exit, starting REPL...") + + IPYSH() + + mcmxi.stop_bots(bots) + S.exit() + +``` + +- then add a tuple containing an instance of a class that contains your fantasy command functions, the prefix of the method +names if any or "", and a lambda method call wrapper to any or all of the bot instance's executables list. Each bot has an +executable list that looks like this: + +``` + self.executables = [ + (self, + "cmd", + lambda event, instance: instance(event) ), + (CARD, + "cmd", + lambda event, instance: instance(event) ), + (EXP_EVAL, + "cmd", + lambda event, instance: instance(event) ), + (HTTP, + "cmd", + lambda event, instance: instance(event) ), + (IRC_TXT, + "cmd", + lambda event, instance: instance(event) ), + (TERM_BIN, + "cmd", + lambda event, instance: instance(event) ), + (UTIL, + "cmd", + lambda event, instance: instance(event) ) ] +``` + +- `CARD` is the singleton class +- `cmd` is the prefix for methods that should be exposed as fantasy commands +- `lambda event, instance: instance(event)` is a method call wrapper (default example) + +When fantasy commands are sent to a channel, an executor defined in the `UTILS` class is invoked, +which is technically also extensible / customizable. The default simply looks for methods in +each of the whitelisted classes (in `self.executables`) for method's prefixed with `cmd` or +whatever prefix is specified, the rest of the fantasy command that was specified corresponds +directly to the rest of the method name:` + +``` + self.executor = ( + lambda bot, obj, method_prefix, call_wrapper, event: call_wrapper( + event, + obj.__getattribute__(next(filter(lambda t: ( + method_prefix != "" + and "{prefix}_{command}" + or "{prefix}{command}").format( + prefix = method_prefix, + command = t[ 0 ] + ) == t[ 1 ] and t[ 1 ], zip(ITER.cycle([event.data( + ).arguments[ 0 ].split(" " + )[ 0 ].strip(next(filter(lambda x: x.channel_name( + ).lower( + ) == event.target().lower( + ), bot.conf.channels() ) + ).command_prefix() ) ] + ), dir(obj) ) ) + )[ 1 ] ) ) ) +``` diff --git a/conf.example.yml b/conf.example.yml new file mode 100644 index 0000000..6dfcd81 --- /dev/null +++ b/conf.example.yml @@ -0,0 +1,108 @@ +bot: + name: 'MMXXI' + dns_servers: + - '4.2.2.1' + - '8.8.4.4' + - '4.2.2.2' + - '8.8.8.8' + log_level: 'info' + networks: + - efnet: + nickname: 'MMXXI' + alt_nickname: 'MMXXI_' + username: 'MMXXI/whatevernet' + password: 'password' + servers: + - 'znc': + retry_connect: true + retry_connect_delay: 2 + retry_connect_max_tries: 5 + address: '127.0.0.1' + port: 6667 + channels: + - '#bot-testing': + command_prefix: "|" + preview_url: true + protect_topic: true + command_delay: 3 + modes: + - '+s' + - '+p' + - '+i' + - '+n' + - '#foo': + command_prefix: "|" + preview_url: false + protect_topic: true + command_delay: 3 + modes: + - '+s' + - '+p' + - '+i' + - '+n' + - '#bar': + ignore: + - 'spammer!*@*' + - 'anotherbot!*@*' + command_prefix: "%" + preview_url: false + command_delay: 3 + modes: + - '+s' + - '+p' + - '+i' + - '+n' + users: + - 'lame': + hostmask: 'lame!lamer@lame.host' + modes: + - '+o' + owners: + - 'lame': + hostmask: 'lame!lamer@lame.host' + users: + - 'default': + hostmask: 'testuser!*@*' + modes: + - '+v' + - 'lame': + hostmask: 'lame!lamer@lame.host' + modes: + - '+o' + - honk: + nickname: 'XXXVII' + username: 'XXXVII/othernetwork' + alt_nickname: 'XXXVII_' + password: 'changeme' + servers: + - 'znc': + address: '127.0.0.1' + port: 7000 + channels: + - '&': + ignore: + - 'CDIX!*@*' + command_prefix: "%" + preview_url: true + command_delay: 3 + modes: + - '+n' + - '+t' + users: + - 'lame': + hostmask: 'lamer!lame@lame.host' + modes: + - '+o' + owners: + - 'lamer': + hostmask: 'lamer!lame@lame.host' + users: + - 'default': + hostmask: 'testuser!*@*' + modes: + - '+v' + - 'lame': + hostmask: 'lame!lamer@lame.host' + modes: + - '+o' + diff --git a/dev.md b/dev.md new file mode 100644 index 0000000..58a73c5 --- /dev/null +++ b/dev.md @@ -0,0 +1,288 @@ +# Notes for development +``` +Various JSON APIs +https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=Craig%20Noone&format=json +https://www.coingecko.com/en/api# +https://api.coingecko.com/api/v3/coins/bitcoin?tickers=true&market_data=true&community_data=true&developer_data=true&sparkline=true +https://searx.everdot.org/?q=hi&categories=general&language=en-US&format=json +https://searx.everdot.org/?q=site%3Atextfiles.com&categories=files&language=en-US&format=json + +
+
+Enter a long URL to make tiny:
+ +
+
+ +bot API docs and examples +https://github.com/jaraco/irc/blob/master/irc/events.py +https://pypi.org/project/unshare/ +https://github.com/docker/docker-py +https://dnspython.readthedocs.io/en/stable/installation.html +https://dnspython.readthedocs.io/en/latest/resolver-class.html +https://github.com/jaraco/irc/blob/71891ea3034067d6678da21bd6d6046ee2d06df5/irc/bot.py#L254 + +character sets +https://jrgraphix.net/research/unicode_blocks.php + +Notes on bs4: +[ meta.attrs['content'] for meta in doc.find_all('meta') if 'name' in meta.attrs and meta.attrs['name'] == 'description' ] +could provide additional info + +https://api.coingecko.com/api/v3/coins/bitcoin?tickers=true&market_data=true&community_data=true&developer_data=true&sparkline=true +notes: +from statistics import mean +set([x['market']['name'] for x in b['tickers']]) +mean(sorted([x['converted_last']['usd'] for x in b['tickers']])) + +In [13]: [ x.attrs for x in HTML(bots[0][0].grab("https://www.youtube.com/watch?v=rvfEldHBnqM"), features = "html.parser").find("head") if type(x) == SOUP.element.Tag if x.attrs.get("property") ! + ...: = None and x.attrs.get("property").startswith("og:") ] +DEBUG|mcmxi->grab https://www.youtube.com/watch?v=rvfEldHBnqM +DEBUG|connectionpool->_new_conn Starting new HTTPS connection (1): www.youtube.com:443 +DEBUG|connectionpool->_make_request https://www.youtube.com:443 "GET /watch?v=rvfEldHBnqM HTTP/1.1" 200 None +DEBUG|mcmxi->grab +Out[13]: +[{'property': 'og:site_name', 'content': 'YouTube'}, + {'property': 'og:url', + 'content': 'https://www.youtube.com/watch?v=rvfEldHBnqM'}, + {'property': 'og:title', 'content': 'Still A G Thang'}, + {'property': 'og:image', + 'content': 'https://i.ytimg.com/vi/rvfEldHBnqM/maxresdefault.jpg'}, + {'property': 'og:image:width', 'content': '1280'}, + {'property': 'og:image:height', 'content': '720'}, + {'property': 'og:description', + 'content': 'Provided to YouTube by Universal Music Group Still A G Thang · Snoop Dogg The Best Of Snoop Dogg ℗ 1998 Priority Records, LLC Released on: 2005-01-01 Associa...'}, + {'property': 'og:type', 'content': 'video.other'}, + {'property': 'og:video:url', + 'content': 'https://www.youtube.com/embed/rvfEldHBnqM'}, + {'property': 'og:video:secure_url', + 'content': 'https://www.youtube.com/embed/rvfEldHBnqM'}, + {'property': 'og:video:type', 'content': 'text/html'}, + {'property': 'og:video:width', 'content': '960'}, + {'property': 'og:video:height', 'content': '720'}, + {'property': 'og:video:tag', 'content': 'Snoop Dogg'}, + {'property': 'og:video:tag', 'content': 'スヌープ・ドッグ'}, + {'property': 'og:video:tag', 'content': 'スヌープドッグ'}, + {'property': 'og:video:tag', 'content': 'The Best Of Snoop Dogg'}, + {'property': 'og:video:tag', 'content': 'Still A G Thang'}, + {'property': 'og:video:tag', 'content': 'スティル・ア・G・サング'}, + {'property': 'og:video:tag', 'content': 'スティル・ア・ジー・サング'}] + +In [23]: (lambda l: [x.attrs for x in HTML(bots[0][0].grab(l), features = "html.parser").find("head") if type(x) == SOUP.element.Tag if x.attrs.get("property") != None and x.attrs.get("property") + ...: .startswith("og:")])("https://www.microsoft.com") +DEBUG|mcmxi->grab https://www.microsoft.com +DEBUG|connectionpool->_new_conn Starting new HTTPS connection (1): www.microsoft.com:443 +DEBUG|connectionpool->_make_request https://www.microsoft.com:443 "GET / HTTP/1.1" 302 0 +DEBUG|mcmxi->grab https://www.microsoft.com/en-au/ +DEBUG|connectionpool->_new_conn Starting new HTTPS connection (1): www.microsoft.com:443 +DEBUG|connectionpool->_make_request https://www.microsoft.com:443 "GET /en-au/ HTTP/1.1" 200 39573 +DEBUG|mcmxi->grab +Out[23]: +[{'property': 'og:url', 'content': 'https://www.microsoft.com/en-au'}, + {'property': 'og:title', 'content': 'Microsoft - Official Home Page'}, + {'property': 'og:description', + 'content': 'At Microsoft our mission and values are to help people and businesses throughout the world realize their full potential.'}, + {'property': 'og:type', 'content': 'website'}] + +In [24]: + +In [77]: [ x for x in HTML(bots[0][0].grab("https://imgur.com/a/AjZCl0b"), features="html.parser").find_all("meta") if type(x) == SOUP.element.Tag if x.get("property") != None and x.get("property + ...: ").startswith("og:")] +DEBUG|mcmxi->grab https://imgur.com/a/AjZCl0b +DEBUG|connectionpool->_new_conn Starting new HTTPS connection (1): imgur.com:443 +DEBUG|connectionpool->_make_request https://imgur.com:443 "GET /a/AjZCl0b HTTP/1.1" 200 None +DEBUG|mcmxi->grab +Out[77]: +[ Imgur: The magic of the Internet , + , + , + , + , + , + , + ] + +``` + +#NLP +- https://medium.com/@ritidass29/create-your-chatbot-using-python-nltk-88809fa621d1 + + +# cards +``` + ''' + clubs (lowest), followed by diamonds, hearts, and spades (highest). + [(chr(x), x) for x in [9827, 9830, 9829, 9824]] + [(chr(x), x) for x in [9831, 9826, 9825, 9828]] + 52 card deck + list(itertools.chain.from_iterable(map(lambda c: [ x for x in [ + (c, "A"), + (c, "K"), + (c, "Q"), + (c, "J"), + (c, "10"), + (c, "9"), + (c, "8"), + (c, "7"), + (c, "6"), + (c, "5"), + (c, "4"), + (c, "3"), + (c, "2") ] + ], [ chr(x) for x in [0x2660, 0x2661, 0x2666, 0x2667 ] ] ) ) ) + + Bridge (for bidding and scoring) and occasionally poker: spades, hearts, diamonds, clubs; 'notrump' + ranks above all the suits[clarification needed] + + Preferans: hearts, diamonds, clubs, spades. Only used for bidding, and No + Trump[clarification needed] is considered higher than hearts. + Five Hundred: hearts, diamonds, clubs, spades (for bidding and scoring) + + Ninety-nine: clubs, hearts, spades, diamonds (supposedly mnemonic as they have + respectively 3, 2, 1, 0 lobes; see article for how this scoring is used) + + Skat: clubs, spades, hearts, diamonds; or acorns, leaves, hearts, bells + (for bidding and to determine which Jack beats which in play) + Big Two: spades, hearts, clubs, diamonds (Presidents reverses suit strength: + hearts, diamonds, spades, clubs) + + Teen patti: In the case where two players have flushes with cards of the same + rank, the winning hand is based on suit color as ranked by clubs, hearts, spades, diamonds. + Thirteen: hearts, diamonds, clubs, spades. + + durak deck: + + durak_deck = lambda: [(x, z, ord(x) | rank_value) for x, z, rank_value in itertools.chain.from_iterable(map(lambda c: [ x for x in [ (c,"A", 0x8192), (c, "K", 0x4096), (c, "Q", 0x2048), (c, "J", 0x1024), (c, "10", 0x512), (c, "9", 0x256), (c, "8", 0x128), (c, "7", 0x64), (c, "6", 0x32), (c, "5", 0x16), (c, "4", 0x08), (c, "3", 0x04), (c, "2", 0x02) ] ], [ chr(x) for x in [0x2660, 0x2661, 0x2666, 0x2667 ] ] ) ) if len([y for y in ["A", "K", "Q", "J", "10", "9", "8", "7", "6"] if y == z]) > 0 ] + + players = ["deuce", "spigot", "rands"] + + current_game_deck = random.sample(durak_deck(), 36) + + The game is typically played with two to five people, with six players if desired, using a deck of 36 cards, + for example a standard 52-card deck from which the numerical cards 2 through 5 have been removed. + In theory the limit for a game with one deck of 36 cards is six players, but this extends a + considerable advantage to the player who attacks first, and a considerable disadvantage to the + player who defends first. Variants exist that use more than one deck. + + durak_deck = players > 6 and durak_deck * 2 or durak_deck (NOTE to self, color offest inverted?) + + The deck is shuffled, and each player is dealt six cards. The bottom card of the stock is + turned and placed face up on the table, its suit determining the trump suit for the current deal. + For example, if it is the 7 of diamonds, then diamonds rank higher than all plain-suit cards. + The rest of the pack is then placed on half over the turnup and at right angles to it, + so that it remains visible. These cards form the prikup or talon. The turnup remains part + of the talon and is drawn as the last card. Cards discarded due to successful defences are + placed in a discard pile next to the talon. + + dealer = lambda cards, players: [ cards[y : y + 6 ] for x, y in zip(range(players), [ 0, 6, 12, 18, 24, 30 ] ) ] + + hands = dict(zip(players, dealer(current_game_deck, len(players) ) ) ) + + # note this pack/unpack of the deck in a lambda isn't actually necesarry since the turn up / trump card + # is actually considered to be part of the talon and doesn't need to be "popped" but "peeked", + # but the example remains for other games where a card maybe selectively removed at the same time that + # the stock is assigned: + + talon, turn_up = (lambda s: (s, s[ -1 ] ) )(list(itertools.chain.from_iterable(dealer(current_game_deck, + 6 )[ len(players) : 6 ]))) + + self.connection.privmsg(target, "trump card is {1}//{0}".format(turn_up)) + + The player who has the lowest trump card will be the first attacker (note that there is no obligation to + play that lowest trump card as the first card). The player to the attacker's left is always the defender. + After each round of attack play proceeds clockwise. If the attack succeeds (see below), the defender + loses their turn and the attack passes to the player on the defender's left. If the attack fails, + the defender becomes the next attacker. + + In [63]: turn_up Out[64]: ('♠', '9', 9846) + + next(enumerate(sorted(itertools.chain.from_iterable([[(x, suite, rank, rank_val + ) for suite, rank, rank_val in hands.get(x) if suite == turn_up[0] ] for x in hands.keys()] + ), key = lambda k: k[3]))) + + Out[65]: (0, ('deuce', '♠', '6', 9842)) + + for reference the hands in this game are: + + { 'deuce': [ + ('♡', 'A', 42995), + ('♦', 'J', 13926), + ('♠', 'J', 13924), <-- trump + ('♡', '6', 9843), + ('♧', 'A', 42999), + ('♠', '6', 9842)], <-- trump (lowest) + 'spigot': [ + ('♦', 'A', 42998), + ('♡', 'J', 13925), + ('♡', 'Q', 9833), + ('♡', '7', 9829), + ('♦', '10', 10102), + ('♧', 'J', 13927)], + 'rands': [ + ('♦', '8', 10094), + ('♧', '8', 10095), + ('♦', '7', 9830), + ('♦', 'K', 26358), + ('♧', 'K', 26359), + ('♧', '6', 9847)] } + + Cards are ranked 6 7 8 9 10 J Q K A. A trump card of any rank beats all cards in the other three suits. + For example, a 6 of trumps beats an ace of any other suit. + + The attacker opens their turn by playing a card face up on the table as an attacking card. The player to the + attacker's left is the defender. They respond to the attack with a defending card. + + + +``` + +# Chess +## cracked out chess board +``` +(lambda l: [y for y, _, _ in [(l[x], l[x].append(chr(ord(l[x][1]) + 6)), l[x].append(chr(ord(l[x][0]) + 6))) for x in l]])(dict(list(zip(["a", "b", "c", "d", "e", "f", "g", "h"], [[chr(0x2656), chr(0x2659)] + [None] * 4, [chr(0x2658), chr(0x2659)] + [None] * 4, [chr(0x2657), chr(0x2659)] + [None] * 4, [chr(0x2655), chr(0x2659)] + [None] * 4, [chr(0x2654), chr(0x2659)] + [None] * 4, [chr(0x2657), chr(0x2659)] + [None] * 4, [chr(0x2658), chr(0x2659)] + [None] * 4, [chr(0x2656), chr(0x2659)] + [None] * 4] ) ) ) ) + +``` + + +# more notes + +``` +In [12]: test = JSON.loads(bots[0][0].grab("https://en.wikipedia.org/w/api.php?action=query + ...: &list=search&srsearch=wtf&format=json") ) +I-search backward: test = + +In [11]: next(enumerate(test.get("query").get("search"))) +Out[11]: +(0, + {'ns': 0, + 'title': 'WTF', + 'pageid': 423373, + 'size': 2534, + 'wordcount': 333, + 'snippet': 'WTF usually refers to: "What the fuck?", an expression of disbelief WTF may also refer to: Work Time Fun, a video game for the PlayStation Portable WTF', + 'timestamp': '2020-08-04T23:25:04Z'}) + +In [12]: + +``` + +## executables notes + +``` +(M, +"", +lambda target, event, instance: self.connection.privmsg( +target, +self.is_func_or_meth(instance) +and (_ for _ in () +).throw(Exception("only property references allowed") ) +or str(instance) ) ), +(STAT, +"", +lambda target, event, instance: self.connection.privmsg( +target, +self.is_func_or_meth(instance) +and (_ for _ in () +).throw(Exception("only property references allowed") ) +or str(instance) ) ), +``` diff --git a/mcmxi/__init__.py b/mcmxi/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/mcmxi/__init__.py @@ -0,0 +1 @@ +# diff --git a/mcmxi/cards.py b/mcmxi/cards.py new file mode 100644 index 0000000..98ca52c --- /dev/null +++ b/mcmxi/cards.py @@ -0,0 +1,403 @@ +import logging as L +import random as R +import itertools as ITER +from mcmxi.irc_text_formatting import IRC_TXT +''' +''' +class cards(): + ''' + ''' + card_table = {} + def __init__(self): + self.l = L.getLogger(__name__) + + # Pretty printer for playing card, every other suite alternates color, order is black, red, black, red + # This will work fine for a 52 card deck, if the unicode values of the enumerated value use the correct + # suit code, eg: (black spade) 0x2660, (red diamond) 0x2661... + self.card_formatter = lambda i, c, index_separator=".": ( + """ + {bold} {index} {index_separator} + {_s_} {color} 98,98 {card_edge} {reset} + {color} {color_card_fg},98 {suit} {reset} + {color} 98,98 {separator} {pad} {reset} + {color} {color_card_fg},98 {rank} {reset} + {color} 98,98 {card_edge} {reset} + {_s_} + """).replace( + " ", + "" + ).replace( + "\n", + "" + ).format( + bold = IRC_TXT.IRC_FMT_BOLD, + index = i, + index_separator = index_separator, + color = IRC_TXT.IRC_FMT_COLOR, + color_card_fg = ( len( [x for x in [ 0x2660, 0x2663 ] if chr(x) == c[ 0 ] ] ) > 0 + and 88 + or IRC_TXT.RED[ 3 ][ 0 ] ), + suit = c[ 0 ], + reset = IRC_TXT.IRC_FMT_RESET, + separator = chr(0xFF0F), + card_edge = chr(0x2595), + rank = c[ 1 ], + _s_ = " ", + pad = ( ( c[ 1 ].isdigit() + and int(c[ 1 ]) != 10 + or not c[ 1 ].isdigit() ) + and " " + or "") ) + + # 36 card deck suitable for playing durak, + # format is character / rank / weight mask / suit mask + # Suit mask may also be used for weighting. + self.durak_deck = lambda: [ ( + x, + z, + ord(x[0]) | rank_value, + rank_value + ) for x, z, rank_value, suite_value in ITER.chain.from_iterable(map( + lambda c: [ x for x in [ ( + c, "A", 0x16777216, c[ 1 ], + ), (c[ 0 ], "K", 0x8388608, c[ 1 ], + ), (c[ 0 ], "Q", 0x4194304, c[ 1 ], + ), (c[ 0 ], "J", 0x2097152, c[ 1 ], + ), (c[ 0 ], "10", 0x1048576, c[ 1 ], + ), (c[ 0 ], "9", 0x524288, c[ 1 ], + ), (c[ 0 ], "8", 0x262144, c[ 1 ], + ), (c[ 0 ], "7", 0x131072, c[ 1 ], + ), (c[ 0 ], "6", 0x65536, c[ 1 ], + ), (c[ 0 ], "5", 0x32768, c[ 1 ], + ), (c[ 0 ], "4", 0x16384, c[ 1 ], + ), (c[ 0 ], "3", 0x8192, c[ 1 ], + ), (c[ 0 ], "2", 0x4096, c[ 1 ] ) ] + ], [ (chr(x), w) for x, w in [ + ( 0x2660, 0x16 ), + ( 0x2665, 0x08 ), + ( 0x2666, 0x04 ), + ( 0x2663, 0x02 ) ] ] ) + ) if len([ y for y in [ + "A", + "K", + "Q", + "J", + "10", + "9", + "8", + "7", + "6" + ] if y == z ] ) > 0 ] + + # Durak dealer + self.durak_dealer = lambda cards, players: [ cards[ y : y + 6 + ] for x, y in zip(range(players + ), [ + 0, + 6, + 12, + 18, + 24, + 30 ] ) ] + ''' + Reset the card table + ''' + def cmd_reset_card_table(self, event): + self.card_table[event.target()] = {} + + event.bot().connection.privmsg(event.target(), "".join([chr(x) for x in [ + 0x0028, + 0x256F, + 0x00B0, + 0x25A1, + 0x00B0, + 0x0029, + 0x256F, + 0xFE35, + 0x0020, + 0x253B, + 0x2501, + 0x253B ] ] ) ) + + ''' + Sets the status of the queued card game to started + ''' + def cmd_card_start(self, event): + if self.card_table.get( + event.target() + ) and self.card_table.get( + event.target() + ).get( + "active" + ) and not self.card_table.get( + event.target() + ).get("started"): + if len(self.card_table[event.target()]["players"]) >= self.card_table[event.target()]["min_players"]: + event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{title}{reset} the game has started, dealing...".format( + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + title = self.card_table[event.target()]["title"], + reset = IRC_TXT.IRC_FMT_RESET ) ) + + self.cmd_card_deal(event) + + else: + event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{title}{reset} there are only {count} players joined, {required} more required to start game".format( + prefix = self.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + title = self.card_table[event.target()]["title"], + reset = IRC_TXT.IRC_FMT_RESET, + count = len(self.card_table[event.target()]["players"]), + required = self.card_table[event.target()]["min_players"] + - len(self.card_table[event.target()]["players"] ) ) ) + ''' + deals the first or next hand for the queued card game + ''' + def cmd_card_deal(self, event): + if self.card_table.get( + event.target() + ) and self.card_table.get( + event.target() + ).get( + "active" + ) and not self.card_table.get( + event.target() + ).get("started"): + hands, stock, trump = self.card_table[event.target()]["deal"]( + self.card_table[event.target()]["players"], + self.card_table[event.target()]["current_game_deck"] ) + + self.card_table[event.target()]["hands"] = hands + self.card_table[event.target()]["talon"] = stock + self.card_table[event.target()]["trump"] = trump + + event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{title}{reset} the trump card for this round is {trump_card}".format( + title = self.card_table[event.target()]["title"], + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + trump_card = self.card_formatter( + "", + self.card_table[event.target()]["trump"], "") ) ) + + [ event.bot().connection.notice(x, "".join( [ self.card_formatter( + y, + z + ) for y, z in zip(range(1, len(self.card_table[event.target()]["hands"].get(x) ) + 1 + ), self.card_table[event.target()]["hands"].get(x) ) ] ) + ) for x in self.card_table[event.target()]["hands"].keys() ] + + ''' + takes a turn at the card table + ''' + def cmd_card_play(self, event): + cards = self.card_table.get(event.target() ).get("play_cards")( + event.event_arguments(), + self.card_table.get( + event.target() + ).get( + "hands" + ).get(event.source( + ).split("!")[ 0 ] ), + self.card_table.get(event.target() ).get("cards_played") ) + + if len(cards) > 0: + self.l.debug(cards) + [ (self.card_table.get( + event.target() + ).get( + "hands" + ).get(event.source_nickname().remove( + x + ), self.card_table.get( + event.target() + ).get( + "cards_played" + ).append( ( + event.source_nickname(), + x[ 0 ], + x[ 1 ], + x[ 2 ] ) ) ) ) for x in cards ] + + event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{title}{reset} this round: {played}".format( + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + title = self.card_table.get( + event.target() + ).get( + "title" + ), played = self.card_table.get( + event.target() + ).get( + "end_turn" + )(self.card_table.get( + event.target() + ).get( + "cards_played" + ), self.card_formatter) ) ) + + ''' + Starts a game of Durak (card game) + ''' + def cmd_durak(self, event): + if not self.card_table.get(event.target()).get("active"): + self.cmd_card_game_instructions(event) + + self.card_table[event.target()] = { + "active" : True, + "started" : False, + "title" : "durak", + "current_game_deck" : R.sample(self.durak_deck(), 36), + "players" : [], + "min_players" : 1, + "max_players" : 6, + "talon" : None, + "hands" : None, + "trump" : None, + "cards_played" : [], + "end_turn" : lambda cards_played, formatter: "".join( [ formatter(name, (suit, rank), ":") + for name, suit, rank, _, _ in cards_played ] ), + "can_join" : lambda players, min_players, max_players: len(players) < max_players, + "play_cards" : lambda args, hand, played: ( len(played) % 2 == 0 + and [ ( hand[ int(args[ 0 ].strip()) - 1 ] ) ] ), + "current_player" : lambda hands, played: next(enumerate(sorted(ITER.chain.from_iterable( [ [ ( + x, + suite, + rank, + rank_val + ) for suite, rank, rank_val in hands.get(x + ) if suite == turn_up[ 0 ] ] for x in hands.keys()] ), key = lambda k: k[ 3 ] ) ) ), + "deal" : lambda players, deck: (dict(zip( + players, + self.durak_dealer(deck, len(players) ) ) + ), list(ITER.chain.from_iterable( + self.durak_dealer(deck, 6) ) + )[ len(players) : 36 ], list(ITER.chain.from_iterable( + self.durak_dealer(deck, 6) ) )[ -1 ] ) + } + + event.bot().connection.privmsg( + event.target(), + """ + {prefix} + {_s_} {bold} durak {reset} + {_s_} initialized + {_s_} and + {_s_} waiting + {_s_} for + {_s_} players + {_s_} to + {_s_} join. + {_s_} Use + {_s_} the + {_s_} {bold} {cmd_prefix} card_join {reset} + {_s_} command + {_s_} to + {_s_} join + {_s_} the + {_s_} game + {_s_} and + {_s_} then + {_s_} use + {_s_} the + {_s_} {bold} {cmd_prefix} card_start {reset} + {_s_} command + {_s_} to + {_s_} start + {_s_} the + {_s_} game + {_s_} when + {_s_}ready. + """.replace( + " ", + "" + ).replace( + "\n", + "" + ).format( + _s_ = " ", + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + cmd_prefix = next(filter(lambda f: f.channel_name( + ) == event.target(), event.bot().conf.channels() ) + ).command_prefix() ) ) + + self.cmd_card_join(event) + + ''' + Shows optimal IRC client configuration instructions for playing the card games + ''' + def cmd_card_game_instructions(self, event): + + event.bot().connection.privmsg( + event.target(), + """ + {prefix} + {_s_} {italic} set + {_s_} your + {_s_} client + {_s_} NOTICE's + {_s_} to + {_s_} current + {_s_} window. {reset} + {_s_} {bold} weechat: {reset} + {_s_} /set + {_s_} irc.msgbuffer.notice + {_s_} = + {_s_} current + {_s_} {bold} irssi: {reset} + {_s_} /WINDOW + {_s_} LEVEL + {_s_} +NOTICES + {_s_} ( + {_s_} {italic} more + {_s_} info: {reset} + {_s_} {underline} https://irssi.org/documentation/help/window_properties/ {reset} + {_s_} ) + """.replace( + "\n", + "" + ).replace( + " ", + "" + ).format( + _s_ = " ", + prefix = IRC_TXT.meander, + italic = IRC_TXT.IRC_FMT_ITALIC, + reset = IRC_TXT.IRC_FMT_RESET, + bold = IRC_TXT.IRC_FMT_BOLD, + underline = IRC_TXT.IRC_FMT_UNDERLINE) ) + + ''' + Joins user to the card table + ''' + def cmd_card_join(self, event): + if self.card_table.get( + event.target() + ) and self.card_table.get( + event.target() + ).get( + "active" + ) and not self.card_table.get( + event.target() + ).get( + "started"): + self.card_table[event.target()]["players"].append(event.source_nickname()) + + event.bot().connection.privmsg(event.target(), "{prefix} {bold}durak{reset} {player} joined the game".format( + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + player = event.source_nickname() ) ) + +CARD = cards() diff --git a/mcmxi/expression_evaluators.py b/mcmxi/expression_evaluators.py new file mode 100644 index 0000000..c47ac97 --- /dev/null +++ b/mcmxi/expression_evaluators.py @@ -0,0 +1,39 @@ +import numexpr as NE +import brainfuck as BF +from py_expression_eval import Parser as EVAL + +''' +''' +class expression_evaluators(): + ''' + ''' + def __self__(self): + self.l = L.getLogger(__name__) + + ''' + Math expression evaluator + ''' + def cmd_eval(self, event) -> None: + event.bot().connection.privmsg( + event.target(), + "{}".format(EVAL( + ).parse(" ".join( + event.event_arguments() ) + ).evaluate( + {} ) ) ) + + ''' + Brainfuck interpreter/evaluator + ''' + def cmd_brainfuck(self, event): + raise NotImplementedError() + + event.bot().connection.privmsg( + event.target(), + "{}".format(BF.evaluate("".join(event.event_arguments() ) ) + ).strip( + "\r" + ).strip( + "\n") ) + +EXP_EVAL = expression_evaluators() diff --git a/mcmxi/http_requests.py b/mcmxi/http_requests.py new file mode 100644 index 0000000..eaad41d --- /dev/null +++ b/mcmxi/http_requests.py @@ -0,0 +1,245 @@ +import requests as WWW +import json as JSON +import logging as L +from bs4 import BeautifulSoup as HTML +from mcmxi.irc_text_formatting import IRC_TXT +from mcmxi.utilities import UTIL + +''' +''' +class http_requests(): + ''' + ''' + def __init__(self): + self.l = L.getLogger(__name__) + + ''' + Gets a preview of a link shared by a chatter + ''' + def cmd_display_www_page_title(self, event) -> None: + try: + doc = HTML(UTIL.grab( + event.url(), + event, + ), features = "html.parser" ) + + title = None + + url = "{}".format(event.url() ) + + _title = doc.find("title").text + + self.l.debug(_title) + + if _title != None: + if len(_title.split(" ") ) > 1: + title = "{bold}{title_first_word}{reset} {title_remaining}".format( + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + title_first_word = _title.split(" ")[ 0 ], + title_remaining = " ".join(_title.split(" ")[ 1: ] ) ) + else: + title = "{}".format( + _title.split(" ")[ 0 ] ) + else: + title = "Untitled" + + event.bot().connection.privmsg( + event.target(), + "{prefix} {formatted_title} {underline}{url}{reset}".format( + prefix = IRC_TXT.meander, + formatted_title = title.strip(), + underline = IRC_TXT.IRC_FMT_UNDERLINE, + url = url.strip(), + reset = IRC_TXT.IRC_FMT_RESET ) ) + + return(doc) + + except Exception as ex: + self.l.exception(ex) + + ''' + Look up info for an IP address + ''' + def cmd_ip(self, event) -> None: + res = JSON.loads(WWW.get( + "http://api.bgpview.io/ip/{ip_address}".format( + ip_address = event.event_arguments()[ 0 ]) ).text ) + + self.l.debug(res) + + [ event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{ptr_record}{reset} AS{as_number} {prefix_sample}".format( + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + ptr_record = res.get("data" + ).get("ptr_record"), + as_number = x.get("asn" + ).get("asn"), + prefix_sample = " ".join(set([x.get("prefix" + ) for x in res.get("data" + ).get("prefixes") ] ) ) ) + ) for x in res.get("data").get("prefixes")[ :1 ] ] + + ''' + Look up BGP info by ASN + ''' + def cmd_asn(self, event) -> None: + res = JSON.loads(WWW.get( + "https://api.bgpview.io/asn/{as_number}/prefixes".format( + as_number = int(event.event_arguments()[ 0 ]) ) ).text ) + + paste = TERM_BIN.term_bin(JSON.dumps( + res, + sort_keys = True, + indent = True ) ) + + self.l.debug(paste) + + event.bot().connection.privmsg( + event.target(), + " ".join([ + """ + {prefix} + {_s_}{bold} {name} {reset} + {_s_} {description} + {_s_} {country} + {_s_} {cidr} + {_s_} {parent} + {_s_}{italic} continued: {reset} + {_s_}{underline} {paste_link} {reset} + """.replace( + " ", + "" + ).format( + _s_ = " ", + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + italic = IRC_TXT.IRC_FMT_ITALIC, + underline = IRC_TXT.IRC_FMT_UNDERLINE, + reset = IRC_TXT.IRC_FMT_RESET, + name = x.get("name"), + description = x.get("description"), + country = x.get("country_code"), + cidr = x.get("prefix"), + parent = x.get("parent" + ).get("rir_name"), + paste_link = paste ) for x in list(res.get("data" + ).get("ipv4_prefixes" + ) + res.get("data" + ).get("ipv6_prefixes") + )[ :1 ] ] ).replace( + "\r\n", + "" + ).replace( + "\n", + "") ) + ''' + ''' + def cmd_btc(self, event): + raise NotImplementedError() + + ''' + Gives a free proxy server IP + ''' + def cmd_proxy(self, event) -> {}: + res = JSON.loads(WWW.get( + "http://pubproxy.com/api/proxy").text) + + self.l.debug(res) + + event.bot().connection.privmsg( + event.target(), + "{prefix} {underline}{bold}{protocol_handler}{reset}{underline}://{url}:{port}".format( + prefix = IRC_TXT.meander, + underline = IRC_TXT.IRC_FMT_UNDERLINE, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + protocol_handler = res.get( + "data" + )[ 0 ].get( + "type" + ), + url = res.get( + "data" + )[ 0 ].get( + "ip" + ), + port = res.get("data" + )[ 0 ].get( + "port" + ) ) ) + ''' + ''' + def cmd_wikipedia(self, event) -> None: + raise NotImplementedError() + + resp = JSON.loads(UTIL.grab( + "https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={question}&format=json".format( + question = "".join(event.event_arguments() ) ) ).text ) + + ''' + Searx meta search (YaCY based) + ''' + def cmd_searx(self, event) -> None: + req = WWW.get( + "https://searx.everdot.org/?q={}&format=json".format( + " ".join(event.event_arguments() ) ) ) + + res = JSON.loads(req.text) + + self.l.debug(res) + + event.bot().connection.privmsg( + event.target(), + """ + {prefix} + {_s_}{bold} {results} {reset} {_s_} results + {_s_}{bold} {answers} {reset} {_s_} answers + {_s_}{bold} {corrections} {reset} {_s_} corrections + {_s_}{bold} {info_boxes} {reset} {_s_} info {_s_} boxes + {_s_}{bold} {suggestions} {reset} {_s_} suggestions + {_s_}{bold} {unresponsive_engines} {reset} {_s_} unresponsive {_s_} engines + {_s_} were {_s_} found {_s_} for {_s_} query + {_s_}{italic} {question} {reset} + {_s_}{underline} {url} {reset} + """.replace( + "\n", + " " + ).replace( + " ", + "" + ).strip( + " " + ).format( + _s_ = " ", + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + italic = IRC_TXT.IRC_FMT_ITALIC, + reset = IRC_TXT.IRC_FMT_RESET, + underline = IRC_TXT.IRC_FMT_UNDERLINE, + results = len(res.get( + "results") ) + or 0, + answers = len(res.get( + "answers") ) + or 0, + corrections = len(res.get( + "corrections") ) + or 0, + info_boxes = len(res.get( + "infoboxes") ) + or 0, + suggestions = len(res.get( + "suggestions") ) + or 0, + unresponsive_engines = len(res.get( + "unresponsive_engines") ) + or 0, + question = res.get( + "query"), + url = req.url.replace("&format=json", "") ) ) + +HTTP = http_requests() diff --git a/mcmxi/irc_text_formatting.py b/mcmxi/irc_text_formatting.py new file mode 100644 index 0000000..45cfe3c --- /dev/null +++ b/mcmxi/irc_text_formatting.py @@ -0,0 +1,215 @@ +import itertools as ITER +import logging as L +import textwrap as TXT +''' +''' +class irc_text_formatting(): + ''' + ''' + def __init__(self): + self.l = L.getLogger(__name__) + + ''' + ''' + self.default_bg_color = ITER.cycle([ 99 ]) + + ''' + ''' + self.art = lambda a: [ (x, chr(y) + ) for x, y in zip(range(len(range(0x2500, 0x25FF) ) + ), range(0x2500, 0x25FF) + ) if x == a ][ 0 ][ 1 ] + + ''' + ''' + self.art_reverse = lambda z: [ y for x, y in zip(range( + 0x2500, + 0x25FE + ), range(len(range( + 0x2500, + 0x25FE) ) ) + ) if ord(self.art(y) + ) == z + or self.art(y + ) == z ] + + ''' + Generic error message + ''' + self.fallback = "".join([ chr(x) for x in [ + 0x00AF, + 0x005C, + 0x005F, + 0x0028, + 0x30C4, + 0x0029, + 0x005F, + 0x002F, + 0x00AF ] ] ) + + ''' + Default color map for IRC color interpolation + ''' + self.default_color_map = ( + lambda bg = None: ITER.cycle(ITER.chain.from_iterable([list(map(lambda x: x != 88 and ( + x, + (bg != None) + and next(bg) + or next(self.default_color_map) + ) or (x - 12, 88), x) ) for x in map(lambda z: [index for index in list(list(ITER.accumulate(range( + z, + z + 7 + ), lambda x, y: x == 0 and z or x + 12) ), + ) ], range(16, 28) ) ] ) ) ) + + ''' + IRC text formatting class property names (bound later) + ''' + self.format_chars = [ + "IRC_FMT_BOLD", + "IRC_FMT_COLOR", + "IRC_FMT_ITALIC", + "IRC_FMT_REVERSE_COLOR", + "IRC_FMT_UNDERLINE", + "IRC_FMT_STRIKETHROUGH", + "IRC_FMT_MONOSPACE", + "IRC_FMT_RESET" ] + + ''' + color palette table names, used to store interpolated color palettes as individual lists in class + properties (initialized in constructor) + ''' + self.palletes = [ + "RED", + "ORANGE", + "YELLOW", + "GREEN", + "GREEN_2", + "GREEN_3", + "AQUA", + "BLUE", + "BLUE_2", + "PURPLE", + "PURPLE_2", + "RED_2" ] + + # Color PRIVMSG + self.color_say = ( + lambda bot, message, target, pallete = ITER.cycle( [ (0, 1) ] + ): [bot.connection.privmsg(target, "".join(["{}{},{}{}{}".format( + self.IRC_FMT_COLOR, + x[ 1 ][ 0 ], + x[ 1 ][ 1 ], + x[ 0 ], + self.IRC_FMT_RESET + ) for x in zip(list(index + ), pallete ) ] ) ) for index in TXT.wrap(message, 53) ] ) + + # Random light shades from random color palettes + self.random_light = lambda: ITER.cycle(R.sample([self.__getattribute__(x + )[ 4 ] for x in self.palletes ], len(self.palletes) ) ) + + # Default message prefix art + self.meander = "{}{}{}".format( + self.art(155), + self.art(156), + self.art(159) ) + + # Μαίανδρος (3-liner) + self.meandros = [[ + self.art(84), + self.art(80), + self.art(80), + self.art(80), + self.art(80), + self.art(87), + " ", + self.art(81) + ],[ self.art(81), + " ", + self.art(84), + self.art(80), + self.art(80), + self.art(93), + " ", + self.art(81) + ],[ self.art(81), + " ", + self.art(90), + self.art(80), + self.art(80), + self.art(80), + self.art(80), + self.art(93) ] ] + + # trans flag + self.trans_flag = lambda: ["".join(map(lambda l: "{color}{fg},{bg}{back_fill}{reset}".format( + color = self.IRC_FMT_COLOR, + fg = x[ 1 ][ 0 ], + bg = x[ 1 ][ 0 ], + back_fill = l, + reset = self.IRC_FMT_RESET ), x[ 0 ]) + ) for x in zip( [ [ self.art(136) ] * 30 ] * 5, ( + self.BLUE[ 5 ], + self.RED_2[ 4 ], + self.RED_2[ 6 ], + self.RED_2[ 4 ], + self.BLUE[ 5 ] ) ) ] + + # IRC text formatting escape characters + [ self.__setattr__(y, "{}".format(x) ) for x, y in zip( [ + chr(0x02), + chr(0x03), + chr(0x1D), + chr(0x16), + chr(0x1F), + chr(0x1E), + chr(0x11), + chr(0x0F) + ], self.format_chars ) ] + + # Create color palette class properties from color palette names + [ (not self.__dict__.get(x[ 1 ][ 0 ] + ) and (self.__setattr__(x[ 1 ][ 0 ], list() + ) == None and self.__getattribute__(x[ 1 ][ 0 ]) + ) or self.__getattribute__(x[ 1 ][ 0 ] ) + ).append((x[ 1 ][ 1 ], x[ 1 ][ 2 ] ) + ) for x in zip(range(7 * 12 + ), ITER.cycle(ITER.chain.from_iterable([list(map(lambda x: x[ 1 ] != 88 and (x[ 0 ], x[ 1 ], next(self.default_bg_color) + ) or (x[ 0 ], x[ 1 ] - 12, next(self.default_bg_color) ), x ) ) for x in map(lambda z: [(z[ 1 ], index + ) for index in list(ITER.accumulate(range(z[ 0 ], z[ 0 ] + 7 + ), lambda x, y: x == 0 and z[ 0 ] or x + 12 ) + ) ], zip(range(16, 28), self.palletes ) ) ] ) ) ) ] + + ''' + Simple greeter, runs on !greet command + ''' + def cmd_greet(self, target, event) -> None: + self.l.debug(event) + + self.color_say( + self, + "ohai2u =)", + target, + ITER.cycle(self.GREEN_2) ) + + ''' + Shrug ASCII + ''' + def cmd_shrug(self, target, event) -> None: + self.l.debug(event) + self.color_say( + self, + self.fallback, + target, + self.random_light() ) + + ''' + Gives a random unicode symbol to add to your collection + ''' + def cmd_give(self, target, event) -> None: + self.l.debug(event) + self.connection.action(target, "gives you a {unicode_artifact}".format( + unicode_artifact = R.sample(list(EMO.EMOJI_UNICODE.values() ), 1)[ 0 ]) ) + +IRC_TXT = irc_text_formatting() diff --git a/mcmxi/mcmxi.py b/mcmxi/mcmxi.py new file mode 100644 index 0000000..8bc8df8 --- /dev/null +++ b/mcmxi/mcmxi.py @@ -0,0 +1,295 @@ +import threading as T +import queue as Q +import sys as S + +import irc.client as C +import logging as L +import time as TIME +import itertools as ITER +from IPython import embed as IPYSH +from dns import resolver as DNS +from mcmxi.objects import * +from mcmxi.cards import CARD +from mcmxi.expression_evaluators import EXP_EVAL +from mcmxi.http_requests import HTTP +from mcmxi.irc_text_formatting import IRC_TXT +from mcmxi.term_bin import TERM_BIN +from mcmxi.utilities import UTIL + +''' +''' +class mcmxi(C.SimpleIRCClient): + conf = None + l = None + state = None + + ''' + Whitelisted executable modules, tuple format is: + module + method prefix or empty string + lambda wrapper function + ''' + executables = None + + def __str__(self): + return(self.conf.parent().bot_name()) + + ''' + ''' + def __init__(self, conf): + self.state = bot_state(self) + self.l = L.getLogger(__name__) + self.conf = conf + UTIL.DNS.nameservers = self.conf.parent().name_servers() + + self.executables = [ + ( self, + "cmd", + lambda event, instance: instance(event) ), + ( CARD, + "cmd", + lambda event, instance: instance(event) ), + ( EXP_EVAL, + "cmd", + lambda event, instance: instance(event) ), + ( HTTP, + "cmd", + lambda event, instance: instance(event) ), + ( IRC_TXT, + "cmd", + lambda event, instance: instance(event) ), + ( TERM_BIN, + "cmd", + lambda event, instance: instance(event) ), + ( UTIL, + "cmd", + lambda event, instance: instance(event) ) ] + + self.event_handlers = [ + ( self.state, + "on", + lambda cls, event, prefix: ( + [ getattr(cls, x)( + event) for x in dir(cls) + if x.startswith(prefix) + and "{}_{}".format( + prefix, + event.event_type() + ) == x ] ) ), + ( bot_event, + "on", + lambda cls, event, prefix: ( + [ getattr(cls, x)( + event) for x in dir(cls) + if x.startswith(prefix) + and "{}_{}".format( + prefix, + event.event_type() + ) == x ] ) ), + ( self, + "on", + lambda cls, event, prefix: ( + super()._dispatcher( + cls.connection, event.inner() ) ) ) ] + + C.SimpleIRCClient.__init__(self) + + ''' + appends an event to the event_history list + ''' + def add_event_to_event_buffer(self, event): + self.l.info("{}".format(event) ) + + self.state.event_history.append(event) + + return(event) + + ''' + override for _dispatcher method (jaraco super class) + attempts to find an executable callback handler from + self.event_handlers + ''' + def _dispatcher(self, connection, event): + try: + mcmxi_event = bot_event( + event_type = event.type, + bot = self, + data = event.arguments, + target = event.target, + inner = event) + [ ( + event.type, + event.arguments, + cls, + prefix, + wrapper( + cls = cls, + event = self.add_event_to_event_buffer(mcmxi_event), + prefix = prefix) ) for cls, prefix, wrapper in self.event_handlers ] + + except Exception as ex: + self.l.exception(ex) + + ''' + bot channel loop, each channel has it's own thread bot_connected this loop and awaits + events from it's allocated command_queue. The call to get on the command queue + blocks until it receives an item. + ''' + def process_work_queue(self, channel) -> None: + while self.state.bot_connected: + try: + ''' + This is a blocking call, if the queue is empty it waits until + the first event is available before succeeding, good for + this kind of a loop + ''' + event = self.state.command_queue[channel.channel_name()].get() + + if event.is_fantasy_command(): + handled = False + + for module_instance, method_prefix, call_wrapper in self.executables: + try: + self.l.debug("{instance} {prefix} {wrapper} ".format( + instance = module_instance, + prefix = method_prefix, + wrapper = call_wrapper ) ) + + UTIL.executor( + self, + module_instance, + method_prefix, + call_wrapper, + event) + + handled = True + + except StopIteration: + pass + + if not handled: + IRC_TXT.color_say( + self, + IRC_TXT.fallback, + channel.channel_name(), + ITER.cycle(IRC_TXT.GREEN) ) + + elif event.arguments_contain_urls(): + HTTP.cmd_display_www_page_title(event) + + except Exception as ex: + if not self.bot_connected: + self.l.debug("self.bot_connected is False, aborting thread") + return + + IRC_TXT.color_say( + self, + IRC_TXT.fallback, + channel.channel_name(), + ITER.cycle(IRC_TXT.RED) ) + + self.l.exception(ex) + + finally: + if self.state.bot_connected: + TIME.sleep(channel.command_delay() ) + self.state.command_queue[ channel.channel_name() ].task_done() + + ''' + Main bot loop, do not use super().start, + this is abortable whereas super().start + uses a "process_forever" that doesn't + have an abort condition for it's thread + ''' + def start(self): + while not self.state.closing: + if not self.state.bot_connected: + self.state.on_disconnect() + + if (self.state.current_retry_count + 1 + > self.state.current_server.retry_connect_max_tries() ): + break + + continue + + self.reactor.process_once() + + TIME.sleep(0.2) + + ''' + Connect factory + ''' + def connect(self): + try: + self.state.current_server = self.conf.next_server() + + super().connect( + server = self.state.current_server.address(), + port = self.state.current_server.port(), + nickname = self.state.current_server.parent().nickname(), + password = self.state.current_server.parent().password(), + username = self.state.current_server.parent().username() ) + + self.state.bot_connected = True + + except Exception as ex: + self.l.debug("connection failed {} ".format(ex)) + self.state.bot_connected = False + + finally: + return(self) + + ''' + Disconnects the IRC connection and shuts down all of the bot threads + ''' + def disconnect(self, closing = None): + try: + if self.state.closing != None: + self.state.closing = True + + if self.state.bot_connected: + self.state.bot_connected = False + + # process_work_queue blocks on a call to queue get, this will unblock it + # and the loop will continue, causing the condition to be re-checked, + # which will cause the loop to break and the thread will abort + [ self.state.command_queue.get(queue).put(None) for queue in self.state.command_queue.keys() ] + + self.connection.quit() + + except Exception as ex: + self.l.exception(ex) + + finally: + return(self) + + ''' + ''' + @staticmethod + def start_bots(): + bots = [] + + bots = [ ( bot, ( lambda t: ( t, t.start() ) + )( T.Thread(target = bot.start) )[ 0 ] + ) for bot in [ mcmxi(config).connect( + ) for config in bot_config().networks() ] ] + + return(bots) + + ''' + ''' + @staticmethod + def stop_bots(bots): + [ bot.disconnect(closing = True) for bot, _ in bots ] + +if __name__ == "__main__": + l = UTIL.setup_logging(bot_config()) + + bots = mcmxi.start_bots() + + l.info("bot is initialized, press C-d at any point for a smooth and easy exit, starting REPL...") + + IPYSH() + + mcmxi.stop_bots(bots) + S.exit() diff --git a/mcmxi/objects/__init__.py b/mcmxi/objects/__init__.py new file mode 100644 index 0000000..7513d93 --- /dev/null +++ b/mcmxi/objects/__init__.py @@ -0,0 +1,18 @@ +# +from mcmxi.objects.bot_event import bot_event +from mcmxi.objects.bot_config import bot_config +from mcmxi.objects.channel import channel +from mcmxi.objects.network import network +from mcmxi.objects.owner import owner +from mcmxi.objects.server import server +from mcmxi.objects.user import user +from mcmxi.objects.bot_state import bot_state + +__all__ = [ "bot_state", + "bot_event", + "bot_config", + "channel", + "network", + "owner", + "server", + "user" ] diff --git a/mcmxi/objects/bot_config.py b/mcmxi/objects/bot_config.py new file mode 100644 index 0000000..98069fb --- /dev/null +++ b/mcmxi/objects/bot_config.py @@ -0,0 +1,41 @@ +import yaml as Y +from mcmxi.objects.network import network + +''' +''' +class bot_config(): + ''' + ''' + def __init__(self, + conf = Y.load( + open('conf.yml'), + Loader = Y.FullLoader + ).get('bot') ): + self.conf = conf + self._networks = [ network(list(zip(*x.items() ) + ), self) for x in self.conf.get('networks') ] + + ''' + ''' + def name_servers(self): + return(self.conf.get('dns_servers') ) + + ''' + ''' + def bot_name(self): + return(self.conf.get('name') ) + + ''' + ''' + def jsonbin_config(self): + return(self.conf.get('jsonbin') ) + + ''' + ''' + def networks(self): + return(self._networks) + + ''' + ''' + def log_level(self): + return(self.conf.get('log_level')) diff --git a/mcmxi/objects/bot_event.py b/mcmxi/objects/bot_event.py new file mode 100644 index 0000000..0286c26 --- /dev/null +++ b/mcmxi/objects/bot_event.py @@ -0,0 +1,1384 @@ +import logging as L +import irc.client as C +''' +''' +class bot_event(): + ''' + ''' + def __init__(self, + event_type, + target, + data, + bot, + url = "", + inner = None): + self._event_type = event_type + self._data = data + self._url = url + self._target = target + self._bot = bot + self._inner = inner + + ''' + ''' + def inner(self): + return(self._inner) + + ''' + ''' + def bot(self): + return(self._bot) + + ''' + ''' + def target(self): + return(self._target) + + ''' + ''' + def data(self): + return(self._data) + + ''' + ''' + def source_nickname(self): + return(self._inner.source.split("!")[ 0 ]) + + ''' + ''' + def source(self): + return(self._data.source) + + ''' + ''' + def is_fantasy_command(self): + return(len([x for x in self.bot().conf.channels() + if self._data.arguments[ 0 ].startswith(x.command_prefix()) + and self.target() == x.channel_name() ]) > 0) + + ''' + ''' + def arguments_contain_urls(self): + return(len(list(filter(lambda l: l.startswith("http://") + or l.startswith("https://"), + self.inner().arguments[ 0 ].split(" ") ) ) ) > 0) + ''' + ''' + def event_arguments(self): + return([ x for x in self._data.arguments[ 0 ].split(" ")[ 1 : ] if x.strip() != "" ]) + + ''' + ''' + def all_arguments(self): + return(self_data.arguments[ 0 ].split(" ") ) + + ''' + ''' + def event_type(self): + return(self._event_type) + + ''' + ''' + def url(self): + return(self._url) + + ''' + ''' + def __str__(self): + return(self._event_type == "pubmsg" + and "{channel} <{source_name}> {pubmsg}".format( + channel = self._target, + source_name = self.source_nickname(), + pubmsg = "".join(self._inner.arguments) ) + or ''' + bot: {bot} + type: {type} + target: {target} + data: {data} + '''.format(bot = self._bot, + type = self._event_type, + target = self._target, + data = self._data) ) + + ''' + ''' + @staticmethod + def on_nick(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_topic(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_action(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_pong(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_invite(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_quit(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_pubnotice(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_pubmsg(event): + + if event.bot().state.command_queue_has_unfinished_tasks(): + raise Exception("command queue is full") + + if event.is_fantasy_command(): + ( lambda e: event.bot().state.command_queue[event.target()].put_nowait(e) )(event) + + elif len(list(filter(lambda x: x.channel_name().lower() == event.target.lower( + ) and x.url_preview() == True, event.bot().conf.channels( + ) ) ) ) != 0 and event.arguments_contain_urls(): + [ ( lambda e: self.command_queue[event.target()].put_nowait(e) )(event) + for x in filter(lambda l: l.startswith("http://") + or l.startswith("https://"), + event.inner().arguments[ 0 ].split(" ") ) if UTIL.verify_address(x) ] + + ''' + ''' + @staticmethod + def on_privnotice(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_privmsg(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_ping(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_part(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_mode(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_kick(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_join(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_error(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_ctcpreply(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_ctcp(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_disconnect(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_dccmsg(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_dcc_disconnect(event): + L.getLogger(__name__).debug("pass") + + ''' + ''' + @staticmethod + def on_dcc_connect(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_welcome(event): + L.getLogger(__name__).debug("pass") + + [ self.bot().connection.join(x.channel_name() ) + for x in self.bot().conf.channels() + if C.is_channel(x.channel_name() ) ] + ''' + ''' + @staticmethod + def on_yourhost(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_created(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_myinfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_featurelist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tracelink(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceconnecting(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tracehandshake(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceunknown(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceoperator(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceuser(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceserver(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceservice(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tracenewtype(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_traceclass(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tracereconnect(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statslinkinfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statscommands(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statscline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsnline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsiline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statskline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsqline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsyline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofstats(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_umodeis(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_serviceinfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofservices(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_service(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_servlist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_servlistend(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statslline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsuptime(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statsoline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_statshline(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserconns(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserclient(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserop(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserunknown(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserchannels(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_luserme(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_adminme(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_adminloc1(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_adminloc2(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_adminemail(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tracelog(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endoftrace(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_tryagain(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_n_local(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_n_global(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_none(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_away(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_userhost(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_ison(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_unaway(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nowaway(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoisuser(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoisserver(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoisoperator(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whowasuser(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofwho(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoischanop(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoisidle(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofwhois(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoischannels(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_liststart(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_list(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_listend(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_channelmodeis(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_channelcreate(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoisaccount(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_notopic(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_currenttopic(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_topicinfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_inviting(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_summoning(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_invitelist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofinvitelist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_exceptlist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofexceptlist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_version(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whoreply(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_namreply(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_whospcrpl(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_killdone(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_closing(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_closeend(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_links(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endoflinks(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofnames(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_banlist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofbanlist(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofwhowas(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_info(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_motd(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_infostart(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofinfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_motdstart(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofmotd(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_motd2(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_youreoper(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_rehashing(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_myportis(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_time(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_usersstart(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_users(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_endofusers(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nousers(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nosuchnick(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nosuchserver(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nosuchchannel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_cannotsendtochan(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_toomanychannels(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_wasnosuchnick(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_toomanytargets(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_noorigin(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_invalidcapcmd(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_norecipient(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_notexttosend(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_notoplevel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_wildtoplevel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_unknowncommand(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nomotd(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_noadmininfo(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_fileerror(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nonicknamegiven(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_erroneusnickname(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nicknameinuse(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nickcollision(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_unavailresource(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_usernotinchannel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_notonchannel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_useronchannel(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nologin(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_summondisabled(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_usersdisabled(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_notregistered(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_needmoreparams(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_alreadyregistered(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nopermforhost(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_passwdmismatch(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_yourebannedcreep(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_youwillbebanned(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_keyset(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_channelisfull(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_unknownmode(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_inviteonlychan(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_bannedfromchan(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_badchannelkey(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_badchanmask(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nochanmodes(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_banlistfull(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_cannotknock(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_noprivileges(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_chanoprivsneeded(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_cantkillserver(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_restricted(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_uniqopprivsneeded(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_nooperhost(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_noservicehost(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_umodeunknownflag(event): + L.getLogger(__name__).debug("pass") + + + ''' + ''' + @staticmethod + def on_usersdontmatch(event): + L.getLogger(__name__).debug("pass") + + diff --git a/mcmxi/objects/bot_state.py b/mcmxi/objects/bot_state.py new file mode 100644 index 0000000..54e4413 --- /dev/null +++ b/mcmxi/objects/bot_state.py @@ -0,0 +1,101 @@ +import collections as COL +import logging as L +import time as TIME +from mcmxi.objects.channel import channel + +class bot_state(): + command_queue = {} + channel_threads = {} + targets = [] + event_history = COL.deque(maxlen = 1000) + bot_connected = False + closing = False + current_retry_count = 0 + l = L.getLogger(__name__) + + ''' + returns true if command queue has unfinished tasks or false if not + ''' + def command_queue_has_unfinished_tasks(self, event): + return(self.command_queue[ event.target() ].unfinished_tasks > 0) + + ''' + ''' + def __init__(self, bot): + self.bot = bot + + ''' + channel joined event + ''' + def on_join(self, event): + if (event.source_nickname() == event.bot().connection.get_nickname() + and len([x for x in self.targets if x == event.target ]) == 0): + + c = [x for x in event.bot().conf.channels() if x.channel_name() == event.target()] + + self.targets.append(c != None + and next(enumerate(c)) + or channel(None, None, target = event.target() ) ) + + if self.command_queue.get(event.target()) == None: + self.command_queue[event.target()] = Q.Queue(1) + + ''' + start channel worker thread + ''' + if self.channel_threads.get(event.target()) == None: + chan = T.Thread(target = lambda: event.bot().process_work_queue(event.target()) ) + + chan.start() + + self.channel_threads[event.target()] = chan + + ''' + Disconnected event + ''' + def on_disconnect(self, event = None) -> None: + if self.bot_connected: + self.bot_connected = False + + while not self.closing and not self.bot_connected: + if self.bot.connect().state.bot_connected: + self.l.debug("connected") + self.current_retry_count = 0 + break + + if self.current_retry_count + 1 > self.current_server.retry_connect_max_tries(): + self.l.debug("max retry count exceeded, giving up") + break + + self.current_retry_count = self.current_retry_count + 1 + + self.l.debug("reconnect failed try {} of {} will retry in {} seconds".format( + self.current_retry_count, + self.current_server.retry_connect_max_tries(), + self.current_server.retry_connect_delay() ) ) + + TIME.sleep(self.current_server.retry_connect_delay() ) + + ''' + NAMES list (response) for a channel event + ''' + def on_namreply(self, event) -> None: + ch_type, channel, nick_list = event.inner().arguments + + if channel == '*': + # User is not in any visible channel + # http://tools.ietf.org/html/rfc2812#section-3.2.5 + self.l.debug("user is not in any visible channel") + return + + c = next(enumerate([x for x in event.bot().conf.channels() if x.channel_name() == event.target()] ) ) + + for nick in nick_list.split(): + nick_modes = [] + + if nick[ 0 ] in event.bot().connection.features.prefix: + nick_modes.append(self.connection.features.prefix[nick[ 0 ] ] ) + nick = nick[ 1: ] + + for mode in nick_modes: + pass diff --git a/mcmxi/objects/channel.py b/mcmxi/objects/channel.py new file mode 100644 index 0000000..cd2e94e --- /dev/null +++ b/mcmxi/objects/channel.py @@ -0,0 +1,40 @@ + +class channel(): + def __init__(self, chan, parent_config, target=None): + if chan != None: + self._channel_name = chan[0][0] + self.conf = chan[1][0] + self._parent = parent_config + elif target != None: + self._channel_name = target + + + self.mode_mask = 0 + + def parent(self): + return(self._parent) + + def channel_name(self): + return(self._channel_name) + + def modes(self): + return(self.conf.get("modes") ) + + def command_prefix(self): + return(self.conf.get("command_prefix") ) + + def url_preview(self): + return(self.conf.get("preview_url") ) + + def protect_channel_topic(self): + return(self.conf.get("protect_topic") ) + + def ignore_list(self): + return(self.conf.get("ignore") ) + + def is_source_ignored(self, source): + return(len([index for index in self.conf.get("ignore") or [] if index == source]) > 0) + + def command_delay(self): + return(float(self.conf.get("command_delay") or 0.0 ) ) + diff --git a/mcmxi/objects/hostmask.py b/mcmxi/objects/hostmask.py new file mode 100644 index 0000000..cc9f5fd --- /dev/null +++ b/mcmxi/objects/hostmask.py @@ -0,0 +1,8 @@ + +''' +nick!username@hostname +*!username@hostname +''' +class host_mask(): + def __init__(self): + pass diff --git a/mcmxi/objects/network.py b/mcmxi/objects/network.py new file mode 100644 index 0000000..9dfbfb5 --- /dev/null +++ b/mcmxi/objects/network.py @@ -0,0 +1,44 @@ +import itertools as ITER + +from mcmxi.objects.server import server +from mcmxi.objects.owner import owner +from mcmxi.objects.user import user +from mcmxi.objects.channel import channel + +class network(): + def __init__(self, network, parent_config): + self.network_name = network[0][0] + self.conf = network[1][0] + self._servers = ITER.cycle( [server(list(zip(*x.items() ) ), self) + for x in self.conf.get("servers") ] ) + self._owners = [owner(list(zip(*x.items() ) ), self) + for x in self.conf.get("owners") ] + self._users = [user(list(zip(*x.items() ) ), self) + for x in self.conf.get("users") ] + self._channels = [channel(list(zip(*x.items() ) ), self) + for x in self.conf.get("channels") ] + self._parent = parent_config + + def channels(self): + return(self._channels) + + def username(self): + return(self.conf.get("username")) + + def nickname(self): + return(self.conf.get("nickname") ) + + def alt_nickname(self): + return(self.conf.get("alt_nickname") ) + + def next_server(self): + return(next(self._servers) ) + + def password(self): + return(self.conf.get("password") ) + + def owners(self): + return(self._owners) + + def parent(self): + return(self._parent) diff --git a/mcmxi/objects/owner.py b/mcmxi/objects/owner.py new file mode 100644 index 0000000..ba2305c --- /dev/null +++ b/mcmxi/objects/owner.py @@ -0,0 +1,17 @@ +class owner(): + def __init__(self, owner, parent_config): + self._owner_name = owner[0][0] + self.conf = owner[1][0] + self._parent = parent_config + + def hostmask(self): + return(self.conf.get('hostmask') ) + + def owner_name(self): + return(self._owner_name) + + def owner_password(self): + return(self.conf.get('password') ) + + def parent(self): + return(self._parent) diff --git a/mcmxi/objects/server.py b/mcmxi/objects/server.py new file mode 100644 index 0000000..fd793b6 --- /dev/null +++ b/mcmxi/objects/server.py @@ -0,0 +1,23 @@ +class server(): + def __init__(self, server, parent_config): + self.server_name = server[0][0] + self.conf = server[1][0] + self._parent = parent_config + + def address(self): + return(self.conf.get('address') ) + + def port(self): + return(self.conf.get('port') ) + + def parent(self): + return(self._parent) + + def retry_connect(self): + return(self.conf.get('retry_connect') or False) + + def retry_connect_delay(self): + return(self.conf.get('retry_connect_delay') or 1) + + def retry_connect_max_tries(self): + return(self.conf.get('retry_connect_max_tries') or 1) diff --git a/mcmxi/objects/user.py b/mcmxi/objects/user.py new file mode 100644 index 0000000..9423352 --- /dev/null +++ b/mcmxi/objects/user.py @@ -0,0 +1,17 @@ +class user(): + def __init__(self, user, parent_config): + self._user_name = user[0][0] + self.conf = user[1][0] + self._parent = parent_config + + def parent(self): + return(self._parent) + + def user_name(self): + return(self._user_name) + + def hostmask(self): + return(self.conf.get('hostmask') ) + + def modes(self): + return(self.conf.get('modes')) diff --git a/mcmxi/term_bin.py b/mcmxi/term_bin.py new file mode 100644 index 0000000..d9240a7 --- /dev/null +++ b/mcmxi/term_bin.py @@ -0,0 +1,43 @@ +import socket as SOCK +import logging as L + +''' +''' +class term_bin(): + ''' + ''' + def __init__(self): + self.l = L.getLogger(__name__) + + ''' + Posts data to TermBin and returns a URL to the term-paste + ''' + def term_bin(self, data) -> "": + self.l.debug(data) + + sock = SOCK.socket( + SOCK.AF_INET, + SOCK.SOCK_STREAM) + + server_address = ( + 'termbin.com', + 9999) + + sock.connect(server_address) + + sock.sendall(bytes( + "{}".format(data), + "UTF-8") ) + + sock.sendall(bytes( + "\0", + "UTF-8") ) + + return(sock.recv( + 4096 + ).decode( + ).replace( + '\r\n', + '\n') ) + +TERM_BIN = term_bin() diff --git a/mcmxi/utilities.py b/mcmxi/utilities.py new file mode 100644 index 0000000..b97bfa4 --- /dev/null +++ b/mcmxi/utilities.py @@ -0,0 +1,242 @@ +import itertools as ITER +import sys as S +import logging as L +import urllib.parse as URL +import requests as WWW +from pyroute2.iproute import IPRoute as IP +from ipaddress import IPv4Network as N4, IPv6Network as N6 +from mcmxi.irc_text_formatting import IRC_TXT +from dns import resolver as DNS +''' +''' +class utilities(): + DNS = DNS.Resolver() + + ''' + Determine whether type of f is one of the many function or method types + ''' + is_func_or_meth = lambda f: ( + TYPES.FunctionType + == type(f) + or TYPES.BuiltinFunctionType + == type(f) + or TYPES.BuiltinMethodType + == type(f) + or TYPES.MethodType + == type(f) + or TYPES.LambdaType + == type(f) + ) and True or False + + ''' + ''' + def __init__(self): + self.l = L.getLogger(__name__) + + # Executes a method call for a given command, for any + # modules whitelisted in self.executables + self.executor = ( + lambda bot, obj, method_prefix, call_wrapper, event: call_wrapper( + event, + obj.__getattribute__(next(filter(lambda t: ( + method_prefix != "" + and "{prefix}_{command}" + or "{prefix}{command}").format( + prefix = method_prefix, + command = t[ 0 ] + ) == t[ 1 ] and t[ 1 ], zip(ITER.cycle([event.data( + ).arguments[ 0 ].split(" " + )[ 0 ].strip(next(filter(lambda x: x.channel_name( + ).lower( + ) == event.target().lower( + ), bot.conf.channels() ) + ).command_prefix() ) ] + ), dir(obj) ) ) + )[ 1 ] ) ) ) + + ''' + grabs an arbitrary HTTP/s response + ''' + def grab(self, url, event = None, depth = 0) -> "": + self.l.debug(url) + + req = WWW.get( + url, + stream = True, + allow_redirects = False) + + self.l.debug(req) + + if req.is_redirect: + if self.verify_address(req.headers.get("Location" + ) or req.headers.get("location") + ) and depth <= 4: + + return(self.grab(req.headers.get("Location" + ) or req.headers.get("location"), event, depth + 1) ) + + else: + raise Exception("no redirect location specified or too many redirects") + + ret = [] + + try: + [ ret.append(index) for index in req.iter_content( + chunk_size = 8192, + decode_unicode = True) + if ( len(bytes("".join(ret), "UTF-8") ) < 500000 * 4 ) + or ( _ for _ in () ).throw(StopIteration) ] + + except StopIteration: + self.l.debug("max size read") + + return("".join(ret) ) + + ''' + Verify that the destination URL doesn't resolve to something + unreasonable, and that it doesn't resolve to any addresses + assigned to the server running the bot + ''' + def verify_address(self, url) -> bool: + addr = None + try: + addr = N6("{}/128".format(self.DNS.resolve(URL.urlparse(url + ).netloc, rdtype = "AAAA" + ).response.answer.pop( + ).to_text( + ).split( + ).pop() ) ) + + except: + try: + addr = N4("{}/32".format(self.DNS.resolve(URL.urlparse(url + ).netloc, rdtype = "A" + ).response.answer.pop( + ).to_text( + ).split( + ).pop() ) ) + + except Exception as ex: + self.l.exception(ex) + + finally: + return(addr != None + and addr.is_global + and not addr.is_reserved + and not addr.is_multicast + and not addr.is_link_local + and not addr.is_private + and not addr.is_unspecified + and len([x.get("attrs" + )[ 0 ][ 1 ] for x in IP( + ).get_addr() if x.get("attrs")[ 0 ][ 1 ] == addr.network_address]) == 0) + + ''' + Guesses the value type of a parameter (parameterization of calls to method calls from IRC via commands) + ''' + def guess_type(self, s) -> type: + try: + value = AST.literal_eval(s) + + except ValueError or SyntaxError: + self.l.debug("computed type {}".format(str) ) + return str + + else: + self.l.debug("computed type {}".format(type(value) ) ) + return type(value) + + ''' + Sets up logging formatter and handler + ''' + @staticmethod + def setup_logging(conf): + formatter = L.Formatter( + fmt = "%(levelname)s|%(module)s->%(funcName)s %(message)s" ) + + handler = L.StreamHandler(S.stderr) + + handler.setFormatter( + formatter ) + + logger = L.getLogger() + + if conf.log_level() == "debug": + logger.setLevel(L.DEBUG) + elif conf.log_level() == "info": + logger.setLevel(L.INFO) + logger.addHandler(handler) + + return logger + + ''' + Gives information about an expanded CIDR prefix + ''' + def cmd_cidr(self, event) -> None: + [ event.bot().connection.privmsg(event.target(), x) for x in map(lambda c: ''' + {prefix} + {_s_} {_s_} {bold} size: {reset} + {_s_}{underline} {num_addresses} {reset} {_s_} {bold} start: {reset} + {_s_}{underline} {start} {reset} {_s_} {bold} end: {reset} + {_s_}{underline} {end} {reset} {_s_} {bold} netmask: {reset} + {_s_}{underline} {netmask} {reset} {_s_} {bold} global: {reset} + {_s_}{underline} {_global} {reset} {_s_} {bold} private: {reset} + {_s_}{underline} {private} {reset} {_s_} {bold} link-local: {reset} + {_s_}{underline} {link_local} {reset} {_s_} {bold} multicast: {reset} + {_s_}{underline} {multicast} {reset} {_s_} {bold} unspecified: {reset} + {_s_}{underline} {unspecified} {reset} {_s_} {bold} supernet: {reset} + {_s_}{underline} {supernet} {reset} + '''.replace( + "\n", + "" + ).replace( + " ", + "" + ).strip( + " " + ).format( + _s_ = " ", + bold = IRC_TXT.IRC_FMT_BOLD, + underline = IRC_TXT.IRC_FMT_UNDERLINE, + reset = IRC_TXT.IRC_FMT_RESET, + prefix = IRC_TXT.meander, + num_addresses = c.num_addresses, + start = c.network_address.exploded, + end = c.broadcast_address, + netmask = c.with_netmask, + _global = c.is_global, + private = c.is_private, + link_local = c.is_link_local, + multicast = c.is_multicast, + unspecified = c.is_unspecified, + supernet = c.supernet() + ), [ x.find(":") != -1 + and N6(x) + or N4(x) + for x in event.event_arguments() ] ) ] + + ''' + Resolve DNS for a hostname + ''' + def cmd_dns(self, event) -> None: + self.l.debug(event) + + args = event.event_arguments() + rdtype = (len(args) > 1) and args[ 0 ] or "A" + Q = (len(args) > 1) and args[ 1 ] or args[ 0 ] + + answer = self.DNS.resolve( + rdtype = rdtype, + qname = Q) + + event.bot().connection.privmsg( + event.target(), + "{prefix} {bold}{c_name}{reset} @{name_server} rrset: {rr_set} ".format( + prefix = IRC_TXT.meander, + bold = IRC_TXT.IRC_FMT_BOLD, + reset = IRC_TXT.IRC_FMT_RESET, + c_name = answer.canonical_name, + name_server = answer.nameserver, + rr_set = answer.rrset.to_text() ) ) + +UTIL = utilities()