mirror of
https://codeberg.org/squili/missedkeys.git
synced 2024-09-19 11:50:23 -06:00
initial commit
This commit is contained in:
commit
bbc0c03446
6 changed files with 352 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
__pycache__/
|
||||
/.vscode/
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
install poetry
|
||||
run `poetry install`
|
||||
run `poetry run python -m missedkeys -- --help` for help
|
||||
i believe in you!
|
0
missedkeys/__init__.py
Normal file
0
missedkeys/__init__.py
Normal file
174
missedkeys/__main__.py
Normal file
174
missedkeys/__main__.py
Normal file
|
@ -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
|
157
poetry.lock
generated
Normal file
157
poetry.lock
generated
Normal file
|
@ -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"
|
15
pyproject.toml
Normal file
15
pyproject.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[tool.poetry]
|
||||
name = "missedkeys"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
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"
|
Loading…
Reference in a new issue