Repository: noenfugler/LemmyModBot (language: python)

Issue:
22 b bag of words
Converted deep learning model to a local bag-of-words model.
Created our own training dataset.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 82dfa7c..cf8d158 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -109,3 +109,44 @@ jobs:
             ${{ steps.meta.outputs.tags }}
             ghcr.io/${{ steps.repo.outputs.lowercase }}:latest
             ghcr.io/${{ steps.repo.outputs.lowercase }}:${{ needs.version.outputs.version }}
+  pypi:
+    runs-on: ubuntu-latest
+    needs: [version, release]
+    permissions:
+      id-token: write
+    environment:
+      name: pypi
+      url: https://pypi.org/p/lemmymodbot-test
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Find and Replace
+        uses: jacobtomlinson/gha-find-replace@v3
+        with:
+          find: "{{VERSION_PLACEHOLDER}}"
+          replace: "${{ needs.version.outputs.version }}"
+          regex: false
+      - name: Find and Replace
+        uses: jacobtomlinson/gha-find-replace@v3
+        with:
+          find: "{{PACKAGE_NAME_PLACEHOLDER}}"
+          replace: "${{ vars.PYPI_PACKAGE_NAME }}"
+          regex: false
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements.txt
+      - name: Install pypa/build
+        run: >-
+          python3 -m
+          pip install
+          build
+          --user
+      - name: Build a binary wheel and a source tarball
+        run: python3 -m build
+      - name: Publish package
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/Dockerfile b/Dockerfile
index db6c647..a03ef45 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 FROM python:3.10-slim as base
 
 RUN apt update \
-    && apt install -yqq wget git gnupg curl python3-pip
+    && apt install -yqq wget git gnupg curl python3-pip libmagic-dev
 RUN pip3 install pipenv
 
 # Copy source files
diff --git a/Pipfile b/Pipfile
index 1b54bcf..d6914ff 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,16 +4,22 @@ verify_ssl = true
 name = "pypi"
 
 [packages]
-pylemmy = "*"
-numpy = "*"
-pandas = "*"
-torch = "1.13.0"
-torchtext = "0.14.0"
-mypy-extensions = "*"
-matrix-client = "*"
-scikit-learn = "*"
+pylemmy = "0.0.6"
+numpy = "1.26.0"
+pandas = "2.1.1"
+torch = "2.0.1"
+torchtext = "0.15.2"
+mypy-extensions = "1.0.0"
+matrix-client = "0.4.0"
+scikit-learn = "1.3.1"
+pydantic="1.10.13"
 wheel = "*"
 imagehash = "*"
+requests = "2.31.0"
+loguru = "0.7.2"
+aiostream = "0.4.5"
+python-magic = "*"
+pykson = "*"
 
 [dev-packages]
 
diff --git a/Pipfile.lock b/Pipfile.lock
index f640fc4..4135e66 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "dab4a1daafa7d16f32be3f17726a82bab09d090b6048d927885f04e8584b7ed8"
+            "sha256": "5cea13031d7fe9afa9e1235a191447a81fe2c06dc468aa866cd33e8ce2db1628"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -21,6 +21,7 @@
                 "sha256:25b7c2d9c83570d78c0ef5a20e949b7d0b8ea3b0b0a4f22c49d3f721105a6057",
                 "sha256:3ecbf87085230fbcd9605c32ca20c4fb41af02c71d076eab246ea22e35947d88"
             ],
+            "index": "pypi",
             "version": "==0.4.5"
         },
         "certifi": {
@@ -33,129 +34,115 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
-                "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
-                "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
-                "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
-                "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
-                "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
-                "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
-                "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
-                "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
-                "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
-                "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
-                "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
-                "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
-                "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
-                "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
-                "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
-                "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
-                "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
-                "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
-                "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
-                "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
-                "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
-                "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
-                "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
-                "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
-                "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
-                "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
-                "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
-                "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
-                "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
-                "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
-                "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
-                "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
-                "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
-                "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
-                "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
-                "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
-                "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
-                "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
-                "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
-                "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
-                "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
-                "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
-                "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
-                "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
-                "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
-                "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
-                "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
-                "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
-                "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
-                "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
-                "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
-                "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
-                "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
-                "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
-                "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
-                "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
-                "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
-                "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
-                "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
-                "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
-                "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
-                "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
-                "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
-                "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
-                "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
-                "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
-                "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
-                "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
-                "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
-                "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
-                "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
-                "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
-                "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
-                "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
-                "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
-                "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
-                "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
-                "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
-                "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
-                "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
-                "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
-                "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
-                "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
-                "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
-                "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
-                "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
-                "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
-                "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
-                "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
+                "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
+                "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
+                "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
+                "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
+                "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
+                "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
+                "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
+                "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
+                "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
+                "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
+                "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
+                "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
+                "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
+                "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
+                "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
+                "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
+                "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
+                "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
+                "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
+                "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
+                "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
+                "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
+                "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
+                "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
+                "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
+                "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
+                "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
+                "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
+                "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
+                "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
+                "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
+                "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
+                "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
+                "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
+                "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
+                "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
+                "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
+                "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
+                "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
+                "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
+                "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
+                "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
+                "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
+                "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
+                "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
+                "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
+                "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
+                "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
+                "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
+                "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
+                "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
+                "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
+                "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
+                "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
+                "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
+                "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
+                "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
+                "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
+                "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
+                "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
+                "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
+                "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
+                "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
+                "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
+                "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
+                "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
+                "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
+                "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
+                "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
+                "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
+                "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
+                "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
+                "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
+                "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
+                "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
+                "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
+                "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
+                "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
+                "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
+                "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
+                "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
+                "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
+                "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
+                "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
+                "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
+                "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
+                "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
+                "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
+                "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
+                "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
             ],
             "markers": "python_full_version >= '3.7.0'",
-            "version": "==3.3.0"
-        },
-        "cmake": {
-            "hashes": [
-                "sha256:009058bdf4f488709f38eaa5dd0ef0f89c6b9c6b6edd9d5b475a308ef75f80bb",
-                "sha256:1b9067ce0251cba3d4c018f2e1577ba9078e9c1eff6ad607ad5ce867843d4571",
-                "sha256:2fb48c780f1a6a3d19e785ebbb754be79d369e25a1cb81043fab049e709564da",
-                "sha256:499b38c77d52fb1964dbb38d0228fed246263a181939a8e753fde8ca227c8e1e",
-                "sha256:591f6b056527aefec009bc61a388776b2fc62444deb0038112a471031f61aeca",
-                "sha256:68983b09de633fc1ce6ab6bce9a25bfa181e41598e7c6bc0a6c0108773ee01cb",
-                "sha256:72289361866314f73be2ae63ddee224ff70223dcef9feb66d0072bf17e245564",
-                "sha256:7bf96237ba11ce2437dc5e071d96b510120a1be4708c631a64b2f38fb46bbd77",
-                "sha256:8056c99e371ff57229df2068364d7c32fea716cb53b4675f639edfb62663decf",
-                "sha256:810e592b606d05a3080a9c19ea839b13226f62cae447a22485b2365782f6b926",
-                "sha256:8bd1e1fa4fc8de7605c663d9408dceb649112f855aab05cca31fdb72e4d78364",
-                "sha256:9f4a7c7be2a25de5901f045618f41b833ea6c0f647115201d38e4fdf7e2815bc",
-                "sha256:b8a2fcb619b89d1cce7b52828316de9a1f27f0c90c2e39d1eae886428c8ee8c6",
-                "sha256:bd40d46dbad3555d5b3ce054bef24b85f256b19139493773751ab6f2b71c1219",
-                "sha256:bdbf0256f554f68c7b1d9740f5d059daf875b685c81a479cbe69038e84eb2fb9",
-                "sha256:c981aafcca2cd7210bd210ec75710c0f34e1fde1998cdcab812e4133e3ab615d",
-                "sha256:d582ef3e9ff0bd113581c1a32e881d1c2f9a34d2de76c93324a28593a76433db"
-            ],
-            "version": "==3.27.7"
+            "version": "==3.3.2"
         },
         "filelock": {
             "hashes": [
-                "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4",
-                "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"
+                "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
+                "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.13.1"
+        },
+        "fsspec": {
+            "hashes": [
+                "sha256:330c66757591df346ad3091a53bd907e15348c2ba17d63fd54f5c39c4457d2a5",
+                "sha256:346a8f024efeb749d2a5fca7ba8854474b1ff9af7c3faaf636a4548781136529"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==3.12.4"
+            "version": "==2023.10.0"
         },
         "idna": {
             "hashes": [
@@ -173,6 +160,14 @@
             "index": "pypi",
             "version": "==4.3.1"
         },
+        "jdatetime": {
+            "hashes": [
+                "sha256:1dd0ee210160c7bd30002803c443e6260ac602ea65b065652a1d567d3bfdca7a",
+                "sha256:8b56c702406eaf84d2ad673c41180488f8c77f156c3ac0630ce0caabaf6af5b8"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==4.1.1"
+        },
         "jinja2": {
             "hashes": [
                 "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
@@ -189,18 +184,12 @@
             "markers": "python_version >= '3.7'",
             "version": "==1.3.2"
         },
-        "lit": {
-            "hashes": [
-                "sha256:e6049032462be1e2928686cbd4a6cc5b3c545d83ecd078737fe79412c1f3fcc1"
-            ],
-            "version": "==17.0.3"
-        },
         "loguru": {
             "hashes": [
                 "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb",
                 "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"
             ],
-            "markers": "python_version >= '3.5'",
+            "index": "pypi",
             "version": "==0.7.2"
         },
         "markupsafe": {
@@ -290,176 +279,84 @@
                 "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.5'",
             "version": "==1.0.0"
         },
         "networkx": {
             "hashes": [
-                "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36",
-                "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"
+                "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6",
+                "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"
             ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.1"
+            "markers": "python_version >= '3.9'",
+            "version": "==3.2.1"
         },
         "numpy": {
             "hashes": [
-                "sha256:020cdbee66ed46b671429c7265cf00d8ac91c046901c55684954c3958525dab2",
-                "sha256:0621f7daf973d34d18b4e4bafb210bbaf1ef5e0100b5fa750bd9cde84c7ac292",
-                "sha256:0792824ce2f7ea0c82ed2e4fecc29bb86bee0567a080dacaf2e0a01fe7654369",
-                "sha256:09aaee96c2cbdea95de76ecb8a586cb687d281c881f5f17bfc0fb7f5890f6b91",
-                "sha256:166b36197e9debc4e384e9c652ba60c0bacc216d0fc89e78f973a9760b503388",
-                "sha256:186ba67fad3c60dbe8a3abff3b67a91351100f2661c8e2a80364ae6279720299",
-                "sha256:306545e234503a24fe9ae95ebf84d25cba1fdc27db971aa2d9f1ab6bba19a9dd",
-                "sha256:436c8e9a4bdeeee84e3e59614d38c3dbd3235838a877af8c211cfcac8a80b8d3",
-                "sha256:4a873a8180479bc829313e8d9798d5234dfacfc2e8a7ac188418189bb8eafbd2",
-                "sha256:4acc65dd65da28060e206c8f27a573455ed724e6179941edb19f97e58161bb69",
-                "sha256:51be5f8c349fdd1a5568e72713a21f518e7d6707bcf8503b528b88d33b57dc68",
-                "sha256:546b7dd7e22f3c6861463bebb000646fa730e55df5ee4a0224408b5694cc6148",
-                "sha256:5671338034b820c8d58c81ad1dafc0ed5a00771a82fccc71d6438df00302094b",
-                "sha256:637c58b468a69869258b8ae26f4a4c6ff8abffd4a8334c830ffb63e0feefe99a",
-                "sha256:767254ad364991ccfc4d81b8152912e53e103ec192d1bb4ea6b1f5a7117040be",
-                "sha256:7d484292eaeb3e84a51432a94f53578689ffdea3f90e10c8b203a99be5af57d8",
-                "sha256:7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505",
-                "sha256:86f737708b366c36b76e953c46ba5827d8c27b7a8c9d0f471810728e5a2fe57c",
-                "sha256:8c6adc33561bd1d46f81131d5352348350fc23df4d742bb246cdfca606ea1208",
-                "sha256:914b28d3215e0c721dc75db3ad6d62f51f630cb0c277e6b3bcb39519bed10bd8",
-                "sha256:b44e6a09afc12952a7d2a58ca0a2429ee0d49a4f89d83a0a11052da696440e49",
-                "sha256:bb0d9a1aaf5f1cb7967320e80690a1d7ff69f1d47ebc5a9bea013e3a21faec95",
-                "sha256:c0b45c8b65b79337dee5134d038346d30e109e9e2e9d43464a2970e5c0e93229",
-                "sha256:c2e698cb0c6dda9372ea98a0344245ee65bdc1c9dd939cceed6bb91256837896",
-                "sha256:c78a22e95182fb2e7874712433eaa610478a3caf86f28c621708d35fa4fd6e7f",
-                "sha256:e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c",
-                "sha256:e5e18e5b14a7560d8acf1c596688f4dfd19b4f2945b245a71e5af4ddb7422feb",
-                "sha256:eae430ecf5794cb7ae7fa3808740b015aa80747e5266153128ef055975a72b99",
-                "sha256:ee84ca3c58fe48b8ddafdeb1db87388dce2c3c3f701bf447b05e4cfcc3679112",
-                "sha256:f042f66d0b4ae6d48e70e28d487376204d3cbf43b84c03bac57e28dac6151581",
-                "sha256:f8db2f125746e44dce707dd44d4f4efeea8d7e2b43aace3f8d1f235cfa2733dd",
-                "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"
+                "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668",
+                "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9",
+                "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f",
+                "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5",
+                "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53",
+                "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2",
+                "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974",
+                "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f",
+                "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42",
+                "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2",
+                "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af",
+                "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67",
+                "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e",
+                "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c",
+                "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7",
+                "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e",
+                "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908",
+                "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66",
+                "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24",
+                "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b",
+                "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e",
+                "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe",
+                "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a",
+                "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575",
+                "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297",
+                "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104",
+                "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab",
+                "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3",
+                "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244",
+                "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124",
+                "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617",
+                "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c"
             ],
             "index": "pypi",
-            "markers": "python_version < '3.13' and python_version >= '3.9'",
-            "version": "==1.26.0"
-        },
-        "nvidia-cublas-cu11": {
-            "hashes": [
-                "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e",
-                "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.10.3.66"
-        },
-        "nvidia-cuda-cupti-cu11": {
-            "hashes": [
-                "sha256:7cc5b8f91ae5e1389c3c0ad8866b3b016a175e827ea8f162a672990a402ab2b0",
-                "sha256:e0cfd9854e1f2edaa36ca20d21cd0bdd5dcfca4e3b9e130a082e05b33b6c5895"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.7.101"
-        },
-        "nvidia-cuda-nvrtc-cu11": {
-            "hashes": [
-                "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03",
-                "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3",
-                "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.7.99"
-        },
-        "nvidia-cuda-runtime-cu11": {
-            "hashes": [
-                "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7",
-                "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.7.99"
-        },
-        "nvidia-cudnn-cu11": {
-            "hashes": [
-                "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7",
-                "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==8.5.0.96"
-        },
-        "nvidia-cufft-cu11": {
-            "hashes": [
-                "sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81",
-                "sha256:c4d316f17c745ec9c728e30409612eaf77a8404c3733cdf6c9c1569634d1ca03"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==10.9.0.58"
-        },
-        "nvidia-curand-cu11": {
-            "hashes": [
-                "sha256:eecb269c970fa599a2660c9232fa46aaccbf90d9170b96c462e13bcb4d129e2c",
-                "sha256:f742052af0e1e75523bde18895a9ed016ecf1e5aa0ecddfcc3658fd11a1ff417"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==10.2.10.91"
-        },
-        "nvidia-cusolver-cu11": {
-            "hashes": [
-                "sha256:00f70b256add65f8c1eb3b6a65308795a93e7740f6df9e273eccbba770d370c4",
-                "sha256:700b781bfefd57d161443aff9ace1878584b93e0b2cfef3d6e9296d96febbf99",
-                "sha256:72fa7261d755ed55c0074960df5904b65e2326f7adce364cbe4945063c1be412"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.4.0.1"
-        },
-        "nvidia-cusparse-cu11": {
-            "hashes": [
-                "sha256:304a01599534f5186a8ed1c3756879282c72c118bc77dd890dc1ff868cad25b9",
-                "sha256:a3389de714db63321aa11fbec3919271f415ef19fda58aed7f2ede488c32733d"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.7.4.91"
-        },
-        "nvidia-nccl-cu11": {
-            "hashes": [
-                "sha256:5e5534257d1284b8e825bc3a182c6f06acd6eb405e9f89d49340e98cd8f136eb"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==2.14.3"
-        },
-        "nvidia-nvtx-cu11": {
-            "hashes": [
-                "sha256:b22c64eee426a62fc00952b507d6d29cf62b4c9df7a480fcc417e540e05fd5ac",
-                "sha256:dfd7fcb2a91742513027d63a26b757f38dd8b07fecac282c4d132a9d373ff064"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==11.7.91"
+            "version": "==1.26.1"
         },
         "pandas": {
             "hashes": [
-                "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614",
-                "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97",
-                "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2",
-                "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8",
-                "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0",
-                "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750",
-                "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea",
-                "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2",
-                "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2",
-                "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa",
-                "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4",
-                "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb",
-                "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6",
-                "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d",
-                "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e",
-                "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98",
-                "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a",
-                "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0",
-                "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4",
-                "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49",
-                "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd",
-                "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e",
-                "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317",
-                "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b",
-                "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"
+                "sha256:021f09c15e1381e202d95d4a21ece8e7f2bf1388b6d7e9cae09dfe27bd2043d1",
+                "sha256:02954e285e8e2f4006b6f22be6f0df1f1c3c97adbb7ed211c6b483426f20d5c8",
+                "sha256:08d287b68fd28906a94564f15118a7ca8c242e50ae7f8bd91130c362b2108a81",
+                "sha256:24057459f19db9ebb02984c6fdd164a970b31a95f38e4a49cf7615b36a1b532c",
+                "sha256:25c9976c17311388fcd953cb3d0697999b2205333f4e11e669d90ff8d830d429",
+                "sha256:3223f997b6d2ebf9c010260cf3d889848a93f5d22bb4d14cd32638b3d8bba7ad",
+                "sha256:3f76280ce8ec216dde336e55b2b82e883401cf466da0fe3be317c03fb8ee7c7d",
+                "sha256:52867d69a54e71666cd184b04e839cff7dfc8ed0cd6b936995117fdae8790b69",
+                "sha256:52897edc2774d2779fbeb6880d2cfb305daa0b1a29c16b91f531a18918a6e0f3",
+                "sha256:5aa6b86802e8cf7716bf4b4b5a3c99b12d34e9c6a9d06dad254447a620437931",
+                "sha256:5b40c9f494e1f27588c369b9e4a6ca19cd924b3a0e1ef9ef1a8e30a07a438f43",
+                "sha256:65177d1c519b55e5b7f094c660ed357bb7d86e799686bb71653b8a4803d8ff0d",
+                "sha256:6ae6ffbd9d614c20d028c7117ee911fc4e266b4dca2065d5c5909e401f8ff683",
+                "sha256:7ad20d24acf3a0042512b7e8d8fdc2e827126ed519d6bd1ed8e6c14ec8a2c813",
+                "sha256:83c166b9bb27c1715bed94495d9598a7f02950b4749dba9349c1dd2cbf10729d",
+                "sha256:851b5afbb0d62f6129ae891b533aa508cc357d5892c240c91933d945fff15731",
+                "sha256:8d0382645ede2fde352da2a885aac28ec37d38587864c0689b4b2361d17b1d4c",
+                "sha256:a6cf8fcc8a63d333970b950a7331a30544cf59b1a97baf0a7409e09eafc1ac38",
+                "sha256:bbd98dcdcd32f408947afdb3f7434fade6edd408c3077bbce7bd840d654d92c6",
+                "sha256:d594e2ce51b8e0b4074e6644758865dc2bb13fd654450c1eae51201260a539f1",
+                "sha256:e78507adcc730533619de07bfdd1c62b2918a68cd4419ea386e28abf7f6a1e5c",
+                "sha256:e7f12b2de0060b0b858cfec0016e7d980ae5bae455a1746bfcc70929100ee633",
+                "sha256:e90c95abb3285d06f6e4feedafc134306a8eced93cb78e08cf50e224d5ce22e2",
+                "sha256:eff794eeb7883c5aefb1ed572e7ff533ae779f6c6277849eab9e77986e352688",
+                "sha256:fc4944dc004ca6cc701dfa19afb8bdb26ad36b9bed5bcec617d2a11e9cae6902"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.9'",
-            "version": "==2.1.1"
+            "version": "==2.1.2"
         },
         "pillow": {
             "hashes": [
@@ -560,9 +457,17 @@
                 "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5",
                 "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"
             ],
-            "markers": "python_version >= '3.7'",
+            "index": "pypi",
             "version": "==1.10.13"
         },
+        "pykson": {
+            "hashes": [
+                "sha256:4954f79d3206b108d6297bbb6af626d8492babdf3c1dfa035c7298205b43ec0b",
+                "sha256:58feb5a27d512f81f0a590d4acc830b80a9cfcc2de47deea203516aea765f909"
+            ],
+            "index": "pypi",
+            "version": "==0.9.9.8.17"
+        },
         "pylemmy": {
             "hashes": [
                 "sha256:4dd615d375af619ba19ba73866f31bbdc6de1f19df53786493e885dbe6fae03d",
@@ -580,6 +485,14 @@
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.8.2"
         },
+        "python-magic": {
+            "hashes": [
+                "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b",
+                "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"
+            ],
+            "index": "pypi",
+            "version": "==0.4.27"
+        },
         "pytz": {
             "hashes": [
                 "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b",
@@ -623,41 +536,40 @@
                 "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
                 "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
             ],
-            "markers": "python_version >= '3.7'",
+            "index": "pypi",
             "version": "==2.31.0"
         },
         "scikit-learn": {
             "hashes": [
-                "sha256:0c275a06c5190c5ce00af0acbb61c06374087949f643ef32d355ece12c4db043",
-                "sha256:0ce9233cdf0cdcf0858a5849d306490bf6de71fa7603a3835124e386e62f2311",
-                "sha256:0e1aa8f206d0de814b81b41d60c1ce31f7f2c7354597af38fae46d9c47c45122",
-                "sha256:14e8775eba072ab10866a7e0596bc9906873e22c4c370a651223372eb62de180",
-                "sha256:1a231cced3ee3fa04756b4a7ab532dc9417acd581a330adff5f2c01ac2831fcf",
-                "sha256:1ec668ce003a5b3d12d020d2cde0abd64b262ac5f098b5c84cf9657deb9996a8",
-                "sha256:3153612ff8d36fa4e35ef8b897167119213698ea78f3fd130b4068e6f8d2da5a",
-                "sha256:4d379f2b34096105a96bd857b88601dffe7389bd55750f6f29aaa37bc6272eb5",
-                "sha256:52b77cc08bd555969ec5150788ed50276f5ef83abb72e6f469c5b91a0009bbca",
-                "sha256:58b0c2490eff8355dc26e884487bf8edaccf2ba48d09b194fb2f3a026dd64f9d",
-                "sha256:66f7bb1fec37d65f4ef85953e1df5d3c98a0f0141d394dcdaead5a6de9170347",
-                "sha256:6bb9490fdb8e7e00f1354621689187bef3cab289c9b869688f805bf724434755",
-                "sha256:7d8dee8c1f40eeba49a85fe378bdf70a07bb64aba1a08fda1e0f48d27edfc3e6",
-                "sha256:8454d57a22d856f1fbf3091bd86f9ebd4bff89088819886dc0c72f47a6c30652",
-                "sha256:845f81c7ceb4ea6bac64ab1c9f2ce8bef0a84d0f21f3bece2126adcc213dfecd",
-                "sha256:8d993fb70a1d78c9798b8f2f28705bfbfcd546b661f9e2e67aa85f81052b9c53",
-                "sha256:9147a3a4df4d401e618713880be023e36109c85d8569b3bf5377e6cd3fecdeac",
-                "sha256:a15d964d9eb181c79c190d3dbc2fff7338786bf017e9039571418a1d53dab236",
-                "sha256:a683394bc3f80b7c312c27f9b14ebea7766b1f0a34faf1a2e9158d80e860ec26",
-                "sha256:a7135a03af71138669f19bc96e7d0cc8081aed4b3565cc3b131135d65fc642ba",
-                "sha256:c413c2c850241998168bbb3bd1bb59ff03b1195a53864f0b80ab092071af6028",
-                "sha256:c6448c37741145b241eeac617028ba6ec2119e1339b1385c9720dae31367f2be",
-                "sha256:ccbbedae99325628c1d1cbe3916b7ef58a1ce949672d8d39c8b190e10219fd32",
-                "sha256:d2cd3634695ad192bf71645702b3df498bd1e246fc2d529effdb45a06ab028b4",
-                "sha256:ef540e09873e31569bc8b02c8a9f745ee04d8e1263255a15c9969f6f5caa627f",
-                "sha256:f66eddfda9d45dd6cadcd706b65669ce1df84b8549875691b1f403730bdef217"
+                "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107",
+                "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161",
+                "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c",
+                "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d",
+                "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157",
+                "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a",
+                "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb",
+                "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c",
+                "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a",
+                "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c",
+                "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5",
+                "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0",
+                "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b",
+                "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03",
+                "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66",
+                "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9",
+                "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf",
+                "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028",
+                "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93",
+                "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05",
+                "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073",
+                "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525",
+                "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e",
+                "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1",
+                "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0",
+                "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.8'",
-            "version": "==1.3.1"
+            "version": "==1.3.2"
         },
         "scipy": {
             "hashes": [
@@ -690,14 +602,6 @@
             "markers": "python_version < '3.13' and python_version >= '3.9'",
             "version": "==1.11.3"
         },
-        "setuptools": {
-            "hashes": [
-                "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
-                "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==68.2.2"
-        },
         "six": {
             "hashes": [
                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
@@ -807,30 +711,6 @@
             "markers": "python_version >= '3.7'",
             "version": "==4.66.1"
         },
-        "triton": {
-            "hashes": [
-                "sha256:0117722f8c2b579cd429e0bee80f7731ae05f63fe8e9414acd9a679885fcbf42",
-                "sha256:1aca3303629cd3136375b82cb9921727f804e47ebee27b2677fef23005c3851a",
-                "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1",
-                "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505",
-                "sha256:42a0d2c3fc2eab4ba71384f2e785fbfd47aa41ae05fa58bf12cb31dcbd0aeceb",
-                "sha256:47b4d70dc92fb40af553b4460492c31dc7d3a114a979ffb7a5cdedb7eb546c08",
-                "sha256:4c9fc8c89874bc48eb7e7b2107a9b8d2c0bf139778637be5bfccb09191685cfd",
-                "sha256:52c47b72c72693198163ece9d90a721299e4fb3b8e24fd13141e384ad952724f",
-                "sha256:74f118c12b437fb2ca25e1a04759173b517582fcf4c7be11913316c764213656",
-                "sha256:75834f27926eab6c7f00ce73aaf1ab5bfb9bec6eb57ab7c0bfc0a23fac803b4c",
-                "sha256:8f05a7e64e4ca0565535e3d5d3405d7e49f9d308505bb7773d21fb26a4c008c2",
-                "sha256:9618815a8da1d9157514f08f855d9e9ff92e329cd81c0305003eb9ec25cc5add",
-                "sha256:9d4978298b74fcf59a75fe71e535c092b023088933b2f1df933ec32615e4beef",
-                "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd",
-                "sha256:bcd9be5d0c2e45d2b7e6ddc6da20112b6862d69741576f9c3dbaf941d745ecae",
-                "sha256:d2684b6a60b9f174f447f36f933e9a45f31db96cb723723ecd2dcfd1c57b778b",
-                "sha256:e3e13aa8b527c9b642e3a9defcc0fbd8ffbe1c80d8ac8c15a01692478dc64d8a",
-                "sha256:fedce6a381901b1547e0e7e1f2546e4f65dca6d91e2d8a7305a2d1f5551895be"
-            ],
-            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
-            "version": "==2.0.0"
-        },
         "typing-extensions": {
             "hashes": [
                 "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
@@ -852,18 +732,16 @@
                 "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07",
                 "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"
             ],
-            "index": "pypi",
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
             "version": "==1.26.18"
         },
         "wheel": {
             "hashes": [
-                "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985",
-                "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"
+                "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942",
+                "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.7'",
-            "version": "==0.41.2"
+            "version": "==0.41.3"
         }
     },
     "develop": {}
diff --git a/README.md b/README.md
index e3d57bd..7629258 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,26 @@
 # LemmyModBot
-A Lemmy bot written in Python using [pylemmy](https://github.com/dcferreira/pylemmy) to access comment and create reports.  
+[![Build](https://github.com/BenMMcLean/LemmyModBot/actions/workflows/build.yml/badge.svg)](https://github.com/BenMMcLean/LemmyModBot/actions/workflows/build.yml) [![Unit Test](https://github.com/BenMMcLean/LemmyModBot/actions/workflows/test.yml/badge.svg)](https://github.com/BenMMcLean/LemmyModBot/actions/workflows/test.yml)
 
-This bot is a work in progress and is expected to expand and grow.
-
-## Toxic Content Detection
-LemmyModBot can detect and report toxic written content on Lemmy communities using a 512-64-2 bag of words deep text classification Neural Network using Pytorch and a local dataset for training.
-
-The neural network with the supplied training dataset is currently achieving accuracies of around 75-80% in detecting toxic content.  Ongoing addition of ambiguous or misclassified data to the training set is hoped to improve this.
-
-## Regexp
-LemmyModBot can currently detect posts using regular expressions that do not contain a question mark.  This functionality is planned to be expended to run arbitrary regular expressions.
-
-## User Watchlist
-Selected users placed on a watachlist can be reported for moderator review.
+A Lemmy bot written in Python that allows you to automatically moderate communities from toxicity, duplicate posts, and CSAM content.  
 
-## Matrix integration
-Messages can be sent to a matrix room.
-
-## Assessed post and comment history
-History is stored in an SQLite database
-
-## Example Usage
-
-from lemmybot import LemmyBot
+This bot is a work in progress and is expected to expand and grow.
 
-lemmy_world = LemmyBot(rebuild_model=True)
-  
-lemmy_world.run()
+## Running the bot
+A Docker image is provided for the purposes of easily running the moderation bot in a containerised
+environment. To run a bot with just the toxicity detection an example `docker-compose` file has been [provided](docker-compose.example.yml).
+To set up further modules (as detailed further below), mount a replacement `main.py` file at `/app/main.py`.
+
+The bot can also be run un-containerised, either by cloning the repo, or by (in the future) using the pip package.
+
+## Modules
+Different aspects of moderation are divided into "Processors". These scan and report content for a single kind of 
+violation, and can be configured individually. Currently, there are eight different processors:
+* **BlacklistProcessor** - This processor allows moderators to provide a list of blacklisted words. If they are encountered in any comment or post, a report is generated.
+* **MimeWhitelistProcessor** - This processor whitelists MIME-types of files that are permitted in the community. If a comment or post where this doesn't hold is encountered, a report is generated.
+* **MimeBlacklistProcessor** - Same as the previously discussed whitelist, except a blacklist
+* **PhashProcessor** - Calculates and tracks a perceptual hash (phash) for each post. If the image has been uploaded before, the bot posts a comment linking to the duplicates.
+* **TitleConformityProcessor** - Enforces a certain title structure (specified using Regex) and warns a poster if their title does not match.
+* **UserProcessor** - Automatically reports content posted by a listed user.
+* **ToxicityProcessor** - Assesses the toxicity of any post/comment and automatically files a report if it is above a specified threshold.
+* **PhotoDNAProcessor** *(untested)* - Given a [Microsoft PhotoDNA API Key](https://www.microsoft.com/en-us/photodna), this processor checks posts for Child Sexual Abuse Material (CSAM). If any violating content is found, the post is automatically deleted. Reports of this nature are sent to the provided Matrix server. This processor is untested as I am unable to obtain an API key.
  
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
new file mode 100644
index 0000000..ece7274
--- /dev/null
+++ b/docker-compose.example.yml
@@ -0,0 +1,20 @@
+version: '2'
+
+services:
+  lemmy-bot:
+    image: ghcr.io/benmmclean/lemmymodbot:latest
+    container_name: lemmy-bot
+    environment:
+      - LEMMY_USERNAME=
+      - LEMMY_PASSWORD=
+      - LEMMY_INSTANCE=
+      - LEMMY_OWNER_USERNAME=
+      - LEMMY_COMMUNITIES=
+      - MATRIX_INSTANCE=
+      - MATRIX_USERNAME=
+      - MATRIX_PASSWORD=
+      - MATRIX_ROOM=
+    volumes:
+      - lemmy-mod-data:/app/data
+volumes:
+  lemmy-mod-data:
\ No newline at end of file
diff --git a/gitversion.yml b/gitversion.yml
index f48de6a..b774121 100644
--- a/gitversion.yml
+++ b/gitversion.yml
@@ -1,40 +1,21 @@
 mode: ContinuousDeployment
 continuous-delivery-fallback-tag: ''
-next-version: 0.0.1
+tag-prefix: '(pre/)?[vV]'
+next-version: 0.2.0
 branches:
-  master:
+  develop:
     mode: ContinuousDeployment
+    increment: Patch
     tag: ''
-    increment: Minor
     track-merge-target: true
     tracks-release-branches: true
-    regex: ^main
     is-release-branch: false
     prevent-increment-of-merged-branch-version: false
-  release:
-    regex: release?[/]
+  master:
+    tag: ''
     mode: ContinuousDeployment
-    increment: Patch # not working, when merging a bugfix from main
-    tag: 'beta'
+    increment: Minor
     is-release-branch: true
     prevent-increment-of-merged-branch-version: true
-  feature:
-    regex: feature?[/]
-    mode: ContinuousDeployment
-    increment: Inherit
-    tag: 'alpha'
-    is-release-branch: false
-    prevent-increment-of-merged-branch-version: true
-  bugfix:
-    regex: bugfix?[/]
-    mode: ContinuousDeployment
-    increment: Inherit
-    tag: '-bugfix'
-    is-release-branch: false
-    prevent-increment-of-merged-branch-version: true
-    source-branches: ['main', 'feature', 'release']
-  pull-request:
-    mode: ContinuousDeployment
-    tag: '-pr'
 ignore:
-  sha: []
\ No newline at end of file
+  sha: []
diff --git a/lemmymodbot/api/__init__.py b/lemmymodbot/api/__init__.py
index 80e7755..86424d5 100644
--- a/lemmymodbot/api/__init__.py
+++ b/lemmymodbot/api/__init__.py
@@ -1,3 +1,5 @@
 from .endpoints import LemmyModAPI
 from .message import PostMessage, PostRemovePost, PostRemoveComment
-from .lemmy_mod_http import LemmyModHttp
\ No newline at end of file
+from .lemmy_mod_http import LemmyModHttp
+from .photodna_client import PhotoDNAClient, PhotoDNARequest, PhotoDNAResponse
+from .matrix_client import MatrixFacade
diff --git a/lemmymodbot/api/matrix_client.py b/lemmymodbot/api/matrix_client.py
new file mode 100644
index 0000000..0f49a7a
--- /dev/null
+++ b/lemmymodbot/api/matrix_client.py
@@ -0,0 +1,27 @@
+from typing import List, Dict
+
+from matrix_client.client import MatrixClient
+
+
+class MatrixFacade:
+    server: str
+    account: str
+    password: str
+
+    def __init__(self, server: str, account: str, password: str):
+        self.server = server
+        self.account = account
+        self.password = password
+
+    def send_message(self, room_id: str, content: str):
+        client = MatrixClient(self.server)
+        client.login(username=self.account, password=self.password, sync=True)
+        room = client.join_room(room_id)
+        room.send_text(text=content)
+        client.logout()
+
+    def report(self, flags: List[str], extras: Dict[str, str], content: str) -> str:
+        matrix_message = '\n\nMod bot (with L plates) : ' + ', '.join(flags)
+        matrix_message = matrix_message + '\n' + str(extras)
+        matrix_message = matrix_message + '\n' + content
+        return matrix_message
diff --git a/lemmymodbot/api/photodna_client.py b/lemmymodbot/api/photodna_client.py
new file mode 100644
index 0000000..c05b34e
--- /dev/null
+++ b/lemmymodbot/api/photodna_client.py
@@ -0,0 +1,42 @@
+from dataclasses import dataclass
+
+import requests
+from pykson import JsonObject, StringField, BooleanField, Pykson
+
+
+@dataclass
+class PhotoDNARequest(JsonObject):
+    data_representation = StringField(serialized_name="DataRepresentation")
+    value = StringField(serialized_name="Value")
+
+
+@dataclass
+class PhotoDNAResponse:
+    is_match = BooleanField(serialized_name="IsMatch")
+
+
+class PhotoDNAClient:
+    base_url: str = "https://api.microsoftmoderator.com/photodna"
+    api_key: str
+    pykson: Pykson
+
+    def __init__(self, api_key: str):
+        self.api_key = api_key
+        self.pykson = Pykson()
+
+    def _do_match(self, request: PhotoDNARequest) -> PhotoDNAResponse:
+        return self.pykson.from_json(requests.post(
+            f"{self.base_url}/Match",
+            data=self.pykson.to_json(request),
+            headers={
+                "Ocp-Apim-Subscription-Key": self.api_key
+            }
+        ).content.decode("utf-8"), PhotoDNAResponse)
+
+    def check_url(self, url: str) -> PhotoDNAResponse:
+        request = PhotoDNARequest()
+        request.data_representation = "URL"
+        request.value = url
+        return self._do_match(
+            request
+        )
diff --git a/lemmymodbot/config.py b/lemmymodbot/config.py
index d078554..40cd81d 100644
--- a/lemmymodbot/config.py
+++ b/lemmymodbot/config.py
@@ -63,5 +63,5 @@ def environment_config():
             os.getenv("MATRIX_PASSWORD"),
             os.getenv("MATRIX_ROOM")
         ) if os.getenv("MATRIX_INSTANCE") is not None else None,
-        True
-    )
\ No newline at end of file
+        os.getenv("DEBUG") == "True"
+    )
diff --git a/lemmymodbot/lemmybot.py b/lemmymodbot/lemmybot.py
index 6660ee1..3c28631 100644
--- a/lemmymodbot/lemmybot.py
+++ b/lemmymodbot/lemmybot.py
@@ -10,8 +10,8 @@
 from pylemmy import Lemmy
 from pylemmy.models.post import Post
 from pylemmy.models.comment import Comment
-from matrix_client.client import MatrixClient
 
+from . import MatrixFacade
 from .config import Config, environment_config
 from lemmymodbot.processors.base import Processor, Content, ContentType, LemmyHandle
 from .reconnection_manager import ReconnectionDelayManager
@@ -21,6 +21,7 @@
 class LemmyBot:
     processors: List[Processor]
     config: Config
+    matrix_facade: MatrixFacade
     """ LemmyBot is a bot that checks Lemmy posts and comments for toxicity, as well as
     performing regexp matching, user watchlist monitoring amongst other things."""
 
@@ -56,35 +57,11 @@ def __init__(
         self.history_db = Database(db_directory_name, db_file_name)
         self.logger.info("Bot starting!")
         self.mydelay = ReconnectionDelayManager(logger=self.logger)
-
-    def send_message_to_matrix(self, m_server: str, m_account: str, m_password: str, m_room_id: str, m_content: str):
-        """ Send a message to a matrix room
-        parameters:
-        server : string - The name of the server.  e.g. "https://matrix.example.org"
-        account : string - The name of the account e.g. "@alice:example.org"
-        password : string - The password for the account e.g. "my-secret-password"
-        room_id : string - The id of the room e.g. "!my-fave-room:example.org"
-        content : string - The content of the message e.g. "Hello World!"
-        """
-        if m_server is None:
-            return
-
-        # Create a Matrix client instance
-        client = MatrixClient(m_server)
-
-        # Log in
-        # client.login_with_password(username=m_account, password=m_password)
-        client.login(username=m_account, password=m_password, sync=True)
-
-        # Get a room instance
-        room = client.join_room(m_room_id)
-
-        # Send a message synchronously.  Returns a result if needed later
-        room.send_text(text=m_content)
-
-        # Wait for the response before continuing
-        # response.wait_for_response()
-        client.logout()
+        self.matrix_facade = MatrixFacade(
+            self.config.matrix_config.server,
+            self.config.matrix_config.account,
+            self.config.matrix_config.password
+        ) if self.config is not None and self.config.matrix_config is not None else None
 
     def clean_content(self, content: str):
         if content is None:
@@ -104,7 +81,9 @@ def run_processors(self, content: Content, elem: Union[Post, Comment], flags: Li
             result = processor.execute(content, LemmyHandle(
                 self.lemmy,
                 elem,
-                self.history_db
+                self.history_db,
+                self.config,
+                self.matrix_facade
             ))
             if result.flags is not None:
                 flags += result.flags
@@ -119,53 +98,50 @@ def process_comment(self, elem):
 
         comment_id = elem.comment_view.comment.id
         self.logger.info('COMMENT %s: %s', comment_id, elem.comment_view.comment.content)
-        if comment_id == 1313063:
-            pass
-            # Opportunity for some debugging here
-        if not self.history_db.in_comments_list(comment_id):
-            flags = []
-            extras = {}
-
-            content = Content(
-                elem.comment_view.community.name,
-                self.clean_content(elem.comment_view.comment.content),
-                elem.comment_view.creator.actor_id,
-                ContentType.COMMENT
+        if self.history_db.in_comments_list(comment_id):
+            self.logger.info('Comment Already Assessed')
+            return
+
+        flags = []
+        extras = {}
+
+        content = Content(
+            elem.comment_view.community.name,
+            self.clean_content(elem.comment_view.comment.content),
+            elem.comment_view.creator.actor_id,
+            elem.comment_view.comment.path,
+            ContentType.COMMENT
+        )
+
+        flags, extras = self.run_processors(content, elem, flags, extras)
+
+        self.history_db.add_to_comments_list(comment_id, extras)
+        pprint(vars(elem))
+
+        if len(flags) <= 0:
+            self.history_db.add_outcome_to_comment(comment_id, "No report")
+
+        # we found something bad
+        self.logger.info('REPORT FOR COMMENT: %s', flags)
+        # noinspection PyBroadException
+        try:
+            if not self.config.debug_mode:
+                elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
+            self.logger.info('****************\nREPORTED COMMENT\n******************')
+            self.history_db.add_outcome_to_comment(comment_id, "Reported comment for: "
+                                                   + '|'.join(flags))
+        except Exception:
+            self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
+            self.history_db.add_outcome_to_comment(comment_id, "Failed to report comment for: "
+                                                   + '|'.join(flags) + " due to exception :"
+                                                   + traceback.format_exc())
+        if self.matrix_facade is not None:
+            self.matrix_facade.send_message(
+                self.config.matrix_config.room_id,
+                self.matrix_facade.report(flags, extras, str(elem.comment_view.comment))
             )
 
-            flags, extras = self.run_processors(content, elem, flags, extras)
-
-            self.history_db.add_to_comments_list(comment_id, extras)
-            pprint(vars(elem))
-            if len(flags) > 0:
-                # we found something bad
-                self.logger.info('REPORT FOR COMMENT: %s', flags)
-                # noinspection PyBroadException
-                try:
-                    if not self.config.debug_mode:
-                        elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
-                    self.logger.info('****************\nREPORTED COMMENT\n******************')
-                    self.history_db.add_outcome_to_comment(comment_id, "Reported comment for: "
-                                                           + '|'.join(flags))
-                except Exception:
-                    self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
-                    self.history_db.add_outcome_to_comment(comment_id, "Failed to report comment for: "
-                                                           + '|'.join(flags) + " due to exception :"
-                                                           + traceback.format_exc())
-                matrix_message = '\n\nMod bot (with L plates) : ' + ', '.join(flags)
-                matrix_message = matrix_message + '\n' + str(extras)
-                matrix_message = matrix_message + '\n' + str(elem.comment_view.comment)
-                self.send_message_to_matrix(m_server=self.config.matrix_config.server,
-                                            m_account=self.config.matrix_config.account,
-                                            m_password=self.config.matrix_config.password,
-                                            m_room_id=self.config.matrix_config.room_id,
-                                            m_content=matrix_message)
-            else:
-                self.history_db.add_outcome_to_comment(comment_id, "No report")
-
-            sleep(5)
-        else:
-            self.logger.info('Comment Already Assessed')
+        sleep(5)
 
     def process_post(self, elem):
         """Determine if the post is new and if so run through detoxifier.  If toxic, then report.
@@ -173,75 +149,74 @@ def process_post(self, elem):
 
         post_id = elem.post_view.post.id
         self.logger.info('POST %s: %s', post_id, elem.post_view.post.name)
-        if not self.history_db.in_posts_list(post_id):
-            flags = []
-            extras_title = {}
-            extras_body = {}
-            extras = {}
-
-            title_content = Content(
-                elem.post_view.community.name,
-                self.clean_content(elem.post_view.post.name),
-                elem.post_view.creator.actor_id,
-                ContentType.POST_TITLE
-            )
-            body_content = Content(
-                elem.post_view.community.name,
-                self.clean_content(elem.post_view.post.body),
-                elem.post_view.creator.actor_id,
-                ContentType.POST_BODY
-            )
-            link_content = Content(
-                elem.post_view.community.name,
-                elem.post_view.post.url,
-                elem.post_view.creator.actor_id,
-                ContentType.POST_LINK
-            )
+        if self.history_db.in_posts_list(post_id):
+            self.logger.info('Post Already Assessed')
+            return
 
-            flags, extras_title = self.run_processors(title_content, elem, flags, extras_title)
-            if body_content.content is not None:
-                flags, extras_body = self.run_processors(body_content, elem, flags, extras_body)
-            if link_content.content is not None:
-                flags, extras = self.run_processors(link_content, elem, flags, extras)
-
-            self.history_db.add_to_posts_list(
-                post_id,
-                extras_title,
-                extras_body,
-                extras,
-                f"{self.config.instance}/post/{elem.post_view.post.id}"
+        flags = []
+        extras_title = {}
+        extras_body = {}
+        extras = {}
+
+        title_content = Content(
+            elem.post_view.community.name,
+            self.clean_content(elem.post_view.post.name),
+            elem.post_view.creator.actor_id,
+            elem.post_view.post.url,
+            ContentType.POST_TITLE
+        )
+        body_content = Content(
+            elem.post_view.community.name,
+            self.clean_content(elem.post_view.post.body),
+            elem.post_view.creator.actor_id,
+            elem.post_view.post.url,
+            ContentType.POST_BODY
+        )
+        link_content = Content(
+            elem.post_view.community.name,
+            elem.post_view.post.url,
+            elem.post_view.creator.actor_id,
+            elem.post_view.post.url,
+            ContentType.POST_LINK
+        )
+
+        flags, extras_title = self.run_processors(title_content, elem, flags, extras_title)
+        if body_content.content is not None:
+            flags, extras_body = self.run_processors(body_content, elem, flags, extras_body)
+        if link_content.content is not None:
+            flags, extras = self.run_processors(link_content, elem, flags, extras)
+
+        self.history_db.add_to_posts_list(
+            post_id,
+            extras_title,
+            extras_body,
+            extras,
+            f"{self.config.instance}/post/{elem.post_view.post.id}"
+        )
+        pprint(elem)
+        if len(flags) <= 0:
+            self.history_db.add_outcome_to_post(post_id, "No report")
+            return
+
+        self.logger.info('REPORT FOR POST: %s', flags)
+        # noinspection PyBroadException
+        try:
+            if not self.config.debug_mode:
+                elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
+            self.logger.info('****************\nREPORTED POST\n******************')
+            self.history_db.add_outcome_to_post(post_id, "Reported Post for: "
+                                                + '|'.join(flags))
+        except Exception:
+            self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
+            self.history_db.add_outcome_to_comment(post_id, "Failed to report post for: "
+                                                   + '|'.join(flags) + " due to exception :"
+                                                   + traceback.format_exc())
+        if self.matrix_facade is not None:
+            self.matrix_facade.send_message(
+                self.config.matrix_config.room_id,
+                self.matrix_facade.report(flags, extras, str(elem.post_view.post))
             )
-            pprint(elem)
-            if len(flags) > 0:
-                self.logger.info('REPORT FOR POST: %s', flags)
-                # noinspection PyBroadException
-                try:
-                    if not self.config.debug_mode:
-                        elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
-                    self.logger.info('****************\nREPORTED POST\n******************')
-                    self.history_db.add_outcome_to_post(post_id, "Reported Post for: "
-                                                        + '|'.join(flags))
-                except Exception:
-                    self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
-                    self.history_db.add_outcome_to_comment(post_id, "Failed to report post for: "
-                                                           + '|'.join(flags) + " due to exception :"
-                                                           + traceback.format_exc())
-                matrix_message = '\n\nMod bot (with L plates) : ' + ', '.join(flags)
-                matrix_message = matrix_message + '\n' + str(extras_title)
-                matrix_message = matrix_message + '\n' + str(extras_body)
-                matrix_message = matrix_message + '\n' + str(elem.post_view.post)
-                self.send_message_to_matrix(m_server=self.config.matrix_config.server,
-                                            m_account=self.config.matrix_config.account,
-                                            m_password=self.config.matrix_config.password,
-                                            m_room_id=self.config.matrix_config.room_id,
-                                            m_content=matrix_message)
-            else:
-                self.history_db.add_outcome_to_post(post_id, "No report")
-
-            sleep(5)
-
-        else:
-            self.logger.info('Post Already Assessed')
+        sleep(5)
 
     def process_content(self, elem: Union[Post, Comment]):
         """ the main doing function, called when a new post or comment is received"""
diff --git a/lemmymodbot/processors/__init__.py b/lemmymodbot/processors/__init__.py
index 8e970a9..2919561 100644
--- a/lemmymodbot/processors/__init__.py
+++ b/lemmymodbot/processors/__init__.py
@@ -4,3 +4,4 @@
 from .blacklist_processor import BlacklistProcessor
 from .phash_processor import PhashProcessor
 from .title_conformity_processor import TitleConformityProcessor
+from .mime_processor import MimeWhitelistProcessor, MimeBlacklistProcessor
\ No newline at end of file
diff --git a/lemmymodbot/processors/base.py b/lemmymodbot/processors/base.py
index 8e3fc45..c40a646 100644
--- a/lemmymodbot/processors/base.py
+++ b/lemmymodbot/processors/base.py
@@ -1,41 +1,43 @@
 from io import BytesIO
-from typing import List, Any, Optional, Union
+from typing import List, Any, Optional, Union, Dict
+
 from pylemmy import Lemmy
 from pylemmy.models.comment import Comment
 from pylemmy.models.post import Post
 
-from lemmymodbot import config
 from lemmymodbot.api import LemmyModHttp
 from lemmymodbot.database import Database
 
 import requests
-from PIL import Image
+from PIL import Image, UnidentifiedImageError
 import imagehash
 
 
 class LemmyHandle:
 
-    def __init__(self, lemmy: Lemmy, elem: Union[Post, Comment], database: Database):
+    def __init__(self, lemmy: Lemmy, elem: Union[Post, Comment], database: Database, config, matrix_facade):
         self.elem = elem
         self.lemmy = lemmy
         self.lemmy_http = LemmyModHttp(lemmy)
         self.database = database
+        self.config = config
+        self.matrix_facade = matrix_facade
 
     def send_message_to_author(self, content: str):
-        if config.debug_mode:
+        if self.config.debug_mode:
             print(f"{content}")
             return
         actor_id = self.elem.post_view.post.creator_id if isinstance(self.elem, Post) else self.elem.comment_view
         self.lemmy_http.send_message(actor_id, f"{content}\n\nMod bot (with L plates)")
 
     def post_comment(self, content: str):
-        if config.debug_mode:
+        if self.config.debug_mode:
             print(f"{content}")
             return
         self.elem.create_comment(f"{content}\n\nMod bot (with L plates)")
 
     def remove_thing(self, reason: str):
-        if config.debug_mode:
+        if self.config.debug_mode:
             print(f"Remove {reason}")
             return
         if isinstance(self.elem, Post):
@@ -43,9 +45,41 @@ def remove_thing(self, reason: str):
         elif isinstance(self.elem, Comment):
             self.lemmy_http.remove_comment(self.elem.comment_view.comment.id, reason)
 
-    def fetch_image(self, url: str) -> (Image, str):
-        img = Image.open(BytesIO(requests.get(url).content))
-        return img, str(imagehash.phash(img))
+    def _get_url(self) -> Optional[str]:
+        if self.elem is not Post or self.elem.post_view.post.url is None:
+            return None
+        return self.elem.post_view.post.url
+
+    def fetch_image(self, url: str = None) -> (Image, str):
+        if url is None:
+            url = self._get_url()
+        try:
+            img = Image.open(BytesIO(requests.get(url).content))
+            return img, str(imagehash.phash(img))
+        except UnidentifiedImageError:
+            return None, None
+
+    def fetch_content(self, url: str = None) -> (bytes, Dict[str, str]):
+        if url is None:
+            url = self._get_url()
+
+        cont = requests.get(
+            url,
+            allow_redirects=True,
+            headers={
+                "Accepts": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0"
+            }
+        )
+        return cont.content, cont.headers
+
+    def send_message(self, message):
+        if self.matrix_facade is None:
+            return
+        self.matrix_facade.send_message(
+            self.config.matrix_config.room_id,
+            message + "\n\nMod bot (with L plates)"
+        )
 
 
 class ContentType:
@@ -59,22 +93,26 @@ class Content:
     community: str
     content: str
     actor_id: str
+    link_to_content: str
     type: ContentType
 
-    def __init__(self, community: str, content: str, actor_id: str, type: ContentType):
+    def __init__(self, community: str, content: str, actor_id: str, link_to_content: str, type: ContentType):
         self.community = community
         self.content = content
         self.actor_id = actor_id
+        self.link_to_content = link_to_content
         self.type = type
 
 
 class ContentResult:
     flags: List[str]
     extras: Optional[Any]
+    was_deleted: bool
 
-    def __init__(self, flags: List[str], extras: Optional[Any]):
+    def __init__(self, flags: List[str], extras: Optional[Any], was_deleted: bool = False):
         self.flags = flags
         self.extras = extras
+        self.was_deleted = was_deleted
 
     @staticmethod
     def nothing():
diff --git a/lemmymodbot/processors/mime_processor.py b/lemmymodbot/processors/mime_processor.py
new file mode 100644
index 0000000..9d4eb66
--- /dev/null
+++ b/lemmymodbot/processors/mime_processor.py
@@ -0,0 +1,46 @@
+from typing import List, Optional
+
+from magic import Magic
+
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
+
+
+class MimeProcessor(Processor):
+    match: List[str]
+    # Refers to type, as in type/subtype
+    type: Optional[List[str]]
+
+    def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
+        if content.type != ContentType.POST_LINK:
+            return ContentResult.nothing()
+
+        file, headers = handle.fetch_content(content.content)
+        mime = Magic(mime=True).from_buffer(file)
+        components = mime.split("/")
+        return self.handle(mime in self.match, self.type is not None and components[0] in self.type, mime)
+
+    def handle(self, in_match_list: bool, in_type_list: bool, mime: str) -> ContentResult:
+        pass
+
+
+class MimeWhitelistProcessor(MimeProcessor):
+
+    def __init__(self, whitelist: Optional[List[str]] = None, type_whitelist: Optional[List[str]] = None):
+        if whitelist is None:
+            whitelist = []
+        self.match = whitelist
+        self.type = type_whitelist
+
+    def handle(self, in_match_list: bool, in_type_list: bool, mime: str) -> ContentResult:
+        return ContentResult([] if in_match_list or in_type_list else ["not_whitelisted_mime"], {"mime": mime})
+
+
+class MimeBlacklistProcessor(MimeProcessor):
+    def __init__(self, blacklist: Optional[List[str]] = None):
+        if blacklist is None:
+            blacklist = []
+        self.match = blacklist
+
+    def handle(self, in_match_list: bool, in_type_list: bool, mime: str) -> ContentResult:
+        return ContentResult(["blacklisted_mime"] if in_match_list else [], {"mime": mime})
diff --git a/lemmymodbot/processors/phash_processor.py b/lemmymodbot/processors/phash_processor.py
index a7e06c8..1adef8c 100644
--- a/lemmymodbot/processors/phash_processor.py
+++ b/lemmymodbot/processors/phash_processor.py
@@ -13,6 +13,9 @@ def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
             return self._warn_user(handle, phash)
 
         phash = handle.fetch_image(content.content)[1]
+        if phash is None:
+            return ContentResult.nothing()
+        
         if handle.database.phash_exists(phash):
             handle.database.add_phash(content.content, phash)
             return self._warn_user(handle, phash)
diff --git a/lemmymodbot/processors/photodna_processor.py b/lemmymodbot/processors/photodna_processor.py
new file mode 100644
index 0000000..8e8450d
--- /dev/null
+++ b/lemmymodbot/processors/photodna_processor.py
@@ -0,0 +1,29 @@
+import os
+
+from lemmymodbot import PhotoDNAClient
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
+
+
+class PhotoDNAProcessor(Processor):
+    client: PhotoDNAClient
+
+    def setup(self) -> None:
+        self.client = PhotoDNAClient(os.getenv("PHOTODNA_API"))
+
+    def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
+        if content.type != ContentType.POST_LINK:
+            return ContentResult.nothing()
+
+        if self.client.check_url(content.content).is_match:
+            return self._delete_and_warn(handle, content)
+
+        return ContentResult.nothing()
+
+    def _delete_and_warn(self, handle: LemmyHandle, content: Content) -> ContentResult:
+        handle.remove_thing("CSAM content detected")
+        handle.send_message(
+            f"""!!! Important !!!\nCSAM content was detected in the following post:
+            {content.link_to_content}. Please collect metadata on the user and submit a report!"""
+        )
+        return ContentResult(["csam"], {"csam": "True"}, True)
diff --git a/main.py b/main.py
index 8e91dc2..369c022 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,11 @@
-from lemmymodbot import LemmyBot
+from lemmymodbot import LemmyBot, MimeWhitelistProcessor
 from lemmymodbot import ToxicityProcessor
 
 if __name__ == "__main__":
     bot = LemmyBot([
         ToxicityProcessor(),
+        MimeWhitelistProcessor(
+            type_whitelist=["image", "video"]
+        )
     ])
     bot.run()
diff --git a/pyproject.toml b/pyproject.toml
index 49fd1ad..e0c2cd8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,3 +4,9 @@ ignore-init-module-imports = true
 [tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["F401","E402","F403"]
 "**/{tests,docs,tools}/*" = ["E402"]
+
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
diff --git a/requirements.txt b/requirements.txt
index 850a1c3..3ecdb20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,43 +1,33 @@
 -i https://pypi.org/simple
 aiostream==0.4.5
 certifi==2023.7.22 ; python_version >= '3.6'
-charset-normalizer==3.3.0 ; python_full_version >= '3.7.0'
-cmake==3.27.7
-filelock==3.12.4 ; python_version >= '3.8'
+charset-normalizer==3.3.2 ; python_full_version >= '3.7.0'
+filelock==3.13.1 ; python_version >= '3.8'
+fsspec==2023.10.0 ; python_version >= '3.8'
 idna==3.4 ; python_version >= '3.5'
 imagehash==4.3.1
+jdatetime==4.1.1 ; python_version >= '3.7'
 jinja2==3.1.2 ; python_version >= '3.7'
 joblib==1.3.2 ; python_version >= '3.7'
-lit==17.0.3
-loguru==0.7.2 ; python_version >= '3.5'
+loguru==0.7.2
 markupsafe==2.1.3 ; python_version >= '3.7'
 matrix-client==0.4.0
 mpmath==1.3.0
-mypy-extensions==1.0.0 ; python_version >= '3.5'
-networkx==3.1 ; python_version >= '3.8'
-numpy==1.26.0 ; python_version < '3.13' and python_version >= '3.9'
-nvidia-cublas-cu11==11.10.3.66 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cuda-cupti-cu11==11.7.101 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cuda-nvrtc-cu11==11.7.99 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cuda-runtime-cu11==11.7.99 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cudnn-cu11==8.5.0.96 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cufft-cu11==10.9.0.58 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-curand-cu11==10.2.10.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cusolver-cu11==11.4.0.1 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-cusparse-cu11==11.7.4.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-nccl-cu11==2.14.3 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-nvidia-nvtx-cu11==11.7.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
-pandas==2.1.1 ; python_version >= '3.9'
+mypy-extensions==1.0.0
+networkx==3.2.1 ; python_version >= '3.9'
+numpy==1.26.1
+pandas==2.1.2
 pillow==10.1.0 ; python_version >= '3.8'
-pydantic==1.10.13 ; python_version >= '3.7'
+pydantic==1.10.13
+pykson==0.9.9.8.17
 pylemmy==0.0.6 ; python_version >= '3.7'
 python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+python-magic==0.4.27
 pytz==2023.3.post1
 pywavelets==1.4.1 ; python_version >= '3.8'
-requests==2.31.0 ; python_version >= '3.7'
-scikit-learn==1.3.1 ; python_version >= '3.8'
+requests==2.31.0
+scikit-learn==1.3.2
 scipy==1.11.3 ; python_version < '3.13' and python_version >= '3.9'
-setuptools==68.2.2 ; python_version >= '3.8'
 six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
 sympy==1.12 ; python_version >= '3.8'
 threadpoolctl==3.2.0 ; python_version >= '3.8'
@@ -45,8 +35,7 @@ torch==2.0.1 ; python_full_version >= '3.8.0'
 torchdata==0.6.1 ; python_version >= '3.8'
 torchtext==0.15.2 ; python_version >= '3.8'
 tqdm==4.66.1 ; python_version >= '3.7'
-triton==2.0.0 ; platform_system == 'Linux' and platform_machine == 'x86_64'
 typing-extensions==4.8.0 ; python_version >= '3.8'
 tzdata==2023.3 ; python_version >= '2'
 urllib3==1.26.18 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
-wheel==0.41.2 ; python_version >= '3.7'
+wheel==0.41.3
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..efeff69
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,17 @@
+[metadata]
+name = {{PACKAGE_NAME_PLACEHOLDER}}
+description = A bot framework to automate Lemmy moderation tasks
+long_description_content_type = text/markdown
+url = https://github.com/BenMMcLean/LemmyModBot
+project_urls =
+    Bug Tracker = https://github.com/BenMMcLean/LemmyModBot/issues
+    Changelog = https://github.com/BenMMcLean/LemmyModBot/releases
+
+[options]
+package_dir =
+    = lemmymodbot
+packages = find:
+python_requires = >=3.6
+
+[options.packages.find]
+where=lemmymodbot
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f2bbac4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,6 @@
+from setuptools import setup
+
+if __name__ == "__main__":
+    setup(
+        version="{{VERSION_PLACEHOLDER}}"
+    )


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e66fb05..1957333 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,6 +18,7 @@ jobs:
           cache: 'pip'
       - name: Install dependencies
         run: |
+          sudo apt install libmagic-dev
           python -m pip install --upgrade pip
           pip install -r requirements.txt
       - name: Test with pytest
diff --git a/tests/processors/test_phash_processor.py b/tests/processors/test_phash_processor.py
index 963c011..c8dd6e2 100644
--- a/tests/processors/test_phash_processor.py
+++ b/tests/processors/test_phash_processor.py
@@ -14,7 +14,8 @@ def test_execute_post_link(self):
             community="",
             actor_id="",
             content="https://example.com/some-image.jpg",
-            type=ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
         handle.database.url_exists.return_value = None
@@ -32,7 +33,8 @@ def test_execute_existing_phash(self):
             community="",
             actor_id="",
             content="https://example.com/some-image.jpg",
-            type=ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
         handle.database.url_exists.return_value = None
@@ -50,7 +52,8 @@ def test_execute_existing_url(self):
             community="",
             actor_id="",
             content="https://example.com/some-image.jpg",
-            type=ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
         handle.database.url_exists.return_value = "phash_value"
@@ -69,7 +72,8 @@ def test_execute_warning(self, mock_warn_user):
             community="",
             actor_id="",
             content="https://example.com/some-image.jpg",
-            type=ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
         handle.database.url_exists.return_value = None
diff --git a/tests/processors/test_title_conformity_processor.py b/tests/processors/test_title_conformity_processor.py
index 4f17e5a..5317030 100644
--- a/tests/processors/test_title_conformity_processor.py
+++ b/tests/processors/test_title_conformity_processor.py
@@ -16,7 +16,8 @@ def test_execute_no_comment(self):
             "",
             "",
             "",
-            ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         ), self.mock_handle)
 
         self.mock_handle.post_comment.assert_not_called()
diff --git a/tests/processors/test_toxicity_processor.py b/tests/processors/test_toxicity_processor.py
index ecb2695..0e9a775 100644
--- a/tests/processors/test_toxicity_processor.py
+++ b/tests/processors/test_toxicity_processor.py
@@ -12,7 +12,8 @@ def test_execute_post_link(self):
             "",
             "https://example.com/some-link",
             "",
-            ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
 
@@ -29,7 +30,8 @@ def test_execute_non_toxic_content(self):
             "",
             "This is a non-toxic comment.",
             "",
-            ContentType.COMMENT
+            type=ContentType.COMMENT,
+            link_to_content="https://example.com"
         )
         handle = Mock()
 
@@ -49,7 +51,8 @@ def test_execute_potentially_toxic_content(self):
             "",
             "This is a potentially toxic comment.",
             "",
-            ContentType.COMMENT
+            type=ContentType.COMMENT,
+            link_to_content="https://example.com"
         )
         handle = Mock()
 
diff --git a/tests/processors/test_user_processor.py b/tests/processors/test_user_processor.py
index 43d2540..cdb6f2a 100644
--- a/tests/processors/test_user_processor.py
+++ b/tests/processors/test_user_processor.py
@@ -12,7 +12,8 @@ def test_execute_post_link(self):
             "",
             "https://example.com/some-link",
             "",
-            ContentType.POST_LINK
+            type=ContentType.POST_LINK,
+            link_to_content="https://example.com"
         )
         handle = Mock()
 
@@ -27,7 +28,8 @@ def test_execute_user_in_watch_list(self):
             "",
             "https://example.com/some-content",
             "user2",
-            ContentType.COMMENT,
+            type=ContentType.COMMENT,
+            link_to_content="https://example.com"
         )
         handle = Mock()
 
@@ -42,7 +44,8 @@ def test_execute_user_not_in_watch_list(self):
             "",
             "https://example.com/some-content",
             "user3",
-            ContentType.COMMENT
+            type=ContentType.COMMENT,
+            link_to_content="https://example.com"
         )
         handle = Mock()