commit bbc0c03446e65e7ba7ea69066de1d94421db6e14 Author: squili Date: Mon May 22 01:50:04 2023 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddb598d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +/.vscode/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f8f097 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +install poetry +run `poetry install` +run `poetry run python -m missedkeys -- --help` for help +i believe in you! \ No newline at end of file diff --git a/missedkeys/__init__.py b/missedkeys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/missedkeys/__main__.py b/missedkeys/__main__.py new file mode 100644 index 0000000..9e1bf50 --- /dev/null +++ b/missedkeys/__main__.py @@ -0,0 +1,174 @@ +from time import time, sleep +from typing import Any +import argparse +import httpx +import progressbar + + +class MisskeyClient: + def __init__(self, token: str, host: str) -> None: + self.token = token + self.host = host + self.client = httpx.Client() + pass + + def request(self, path: str, body: Any = {}) -> Any: + remove = set() + for key, value in body.items(): + if value is None: + remove.add(key) + for key in remove: + del body[key] + body['i'] = self.token + i = 0 + while True: + try: + response = self.client.post( + f'{self.host}/{path}', json=body) + break + except Exception as e: + if i > 2: + raise e + i += 1 + sleep(1) + if response.status_code >= 400 and response.status_code != 500: + if response.headers.get('Content-Type') == 'application/json': + body = response.json() + else: + body = response.text + raise Exception(f'{response.status_code}: {body}') + if response.status_code != 204: + return response.json() + + +class Interval: + def __init__(self, wait_for: float) -> None: + self.last = time() + self.wait_for = wait_for + + def next(self) -> None: + next_time = self.wait_for + self.last + while next_time > time(): + amount = (next_time - time()) / 2 + if amount > 0: + sleep(amount) + self.last += self.wait_for + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--token', required=True, help='user token - you can get it from the network tab of devtools or generate one from some menu i forgot') + parser.add_argument('--host', required=True, help='specifically the api base, which is normally https://{instance}/api') + parser.add_argument('--max-days', required=True, help='max days a note should last before getting processed') + parser.add_argument('--delay-multiplier', default='1.25', help='set to higher to lower the chance of rate limiting. set to lower to increase chances. it\'s like a casino!') + parser.add_argument('--interactions', default=None, help='if a note has more than this number of interactions, it will not be deleted') + args = parser.parse_args() + client = MisskeyClient(token=args.token, host=args.host) + + # fetch account info + me = client.request('i') + user_id: str = me['id'] + + # fetch exemptions clip + clips = client.request('clips/list') + for clip in clips: + if clip['name'] == 'Missedkeys Exempt': + exemption_clip = clip['id'] + break + else: + clip = client.request('clips/create', { + 'name': 'Missedkeys Exempt', + 'is_public': False, + }) + exemption_clip = clip['id'] + + # fetch exempt notes + exempt_notes = set() + until_id = None + until_date = round(time()*1000 - (1000*60*60*24 * int(args.max_days))) + while True: + chunk = client.request('clips/notes', { + 'clipId': exemption_clip, + 'limit': 100, + 'untilId': until_id, + }) + exempt_notes.update(map(lambda note: note['id'], chunk)) + if len(chunk) != 100: + break + until_id = chunk[-1]['id'] + + # gather notes + all_notes = [] + until_id = None + while True: + chunk: list = client.request('users/notes', { + 'userId': user_id, + 'limit': 100, + 'untilDate': until_date, + 'untilId': until_id, + }) + print('got', len(chunk), 'notes') + all_notes.extend(chunk) + if len(chunk) != 100: + break + until_id = chunk[-1]['id'] + + notes = [] + for note in all_notes: + note_id: str = note['id'] + if note_id in exempt_notes: + continue + if args.interactions: + interactions = 0 + for value in note['reactions'].values(): + interactions += value + interactions += note['renoteCount'] + if interactions > args.interactions: + print(f'adding {note_id} to exemption clip') + client.request('clips/add-note', { + 'clipId': exemption_clip, + 'noteId': note_id, + }) + continue + notes.append(note) + + notes = list(reversed(notes)) # thread heads go first to cascade deletes + + # do + delete_interval = Interval(60 * 2 * float(args.delay_multiplier)) + bar = progressbar.ProgressBar(widgets=[ + progressbar.Percentage(), + ' ', + progressbar.Counter(format='%(value)d/%(max_value)d'), + ' ', + progressbar.Bar(), + ' ', + progressbar.ETA(), + ' | ', + progressbar.Variable('message', format='{formatted_value}') + ], max_value=len(notes)) + for index in range(len(notes)): + note = notes[index] + note_id: str = note['id'] + try: + client.request('notes/show', { + 'noteId': note_id + }) + except Exception as e: + if '404' in e.args[0]: + bar.update(index, message=f'{note_id} already deleted') + continue + raise e + client.request('notes/delete', { + 'noteId': note_id + }) + bar.update(index, message=f'deleting {note_id}') + delete_interval.next() + bar.finish() + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + pass diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2cc7d4d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,157 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "0.17.0" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"}, + {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.24.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"}, + {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.18.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "progressbar2" +version = "4.2.0" +description = "A Python Progressbar library to provide visual (yet text based) progress to long running operations." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "progressbar2-4.2.0-py2.py3-none-any.whl", hash = "sha256:1a8e201211f99a85df55f720b3b6da7fb5c8cdef56792c4547205be2de5ea606"}, + {file = "progressbar2-4.2.0.tar.gz", hash = "sha256:1393922fcb64598944ad457569fbeb4b3ac189ef50b5adb9cef3284e87e394ce"}, +] + +[package.dependencies] +python-utils = ">=3.0.0" + +[package.extras] +docs = ["sphinx (>=1.8.5)"] +tests = ["flake8 (>=3.7.7)", "freezegun (>=0.3.11)", "pytest (>=4.6.9)", "pytest-cov (>=2.6.1)", "pytest-mypy", "sphinx (>=1.8.5)"] + +[[package]] +name = "python-utils" +version = "3.5.2" +description = "Python Utils is a module with some convenient utilities not included with the standard Python install" +category = "main" +optional = false +python-versions = ">3.6.0" +files = [ + {file = "python-utils-3.5.2.tar.gz", hash = "sha256:68198854fc276bc4b2403b261703c218e01ef564dcb072a7096ed9ea7aa5130c"}, + {file = "python_utils-3.5.2-py2.py3-none-any.whl", hash = "sha256:8bfefc3430f1c48408fa0e5958eee51d39840a5a987c2181a579e99ab6fe5ca6"}, +] + +[package.extras] +docs = ["mock", "python-utils", "sphinx"] +loguru = ["loguru"] +tests = ["flake8", "loguru", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mypy", "sphinx", "types-setuptools"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "3fab10f6dae97c57e6adf989aa6a0494ba96231346a6da403c92b2b8947f8a1e" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8957f20 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "missedkeys" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +httpx = "^0.24.0" +progressbar2 = "^4.2.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"