forget-cancellare-vecchi-toot/libforget/misskey.py

192 lines
6.9 KiB
Python
Raw Normal View History

2021-11-09 10:07:56 +01:00
from app import db, sentry
2021-11-10 12:33:55 +01:00
from model import MisskeyApp, MisskeyInstance, Account, OAuthToken, Post
2021-11-09 10:07:56 +01:00
from uuid import uuid4
from hashlib import sha256
from libforget.exceptions import TemporaryError, PermanentError
2021-11-10 07:51:38 +01:00
from libforget.session import make_session
2021-11-09 10:07:56 +01:00
2021-11-10 07:51:38 +01:00
def get_or_create_app(instance_url, callback, website, session):
2021-11-09 10:07:56 +01:00
instance_url = instance_url
app = MisskeyApp.query.get(instance_url)
if not app:
# check if the instance uses https while getting instance infos
try:
2021-11-10 07:51:38 +01:00
r = session.post('https://{}/api/meta'.format(instance_url))
2021-11-09 10:07:56 +01:00
r.raise_for_status()
proto = 'https'
except Exception:
2021-11-10 07:51:38 +01:00
r = session.post('http://{}/api/meta'.format(instance_url))
2021-11-09 10:07:56 +01:00
r.raise_for_status()
proto = 'http'
# This is using the legacy authentication method, because the newer
# Miauth method breaks the ability to log out and log back into forget.
2021-11-09 10:07:56 +01:00
app = MisskeyApp()
app.instance = instance_url
app.protocol = proto
# register the app
r = session.post('{}://{}/api/app/create'.format(app.protocol, app.instance), json = {
'name': 'forget',
'description': website,
2022-03-02 13:30:40 +01:00
'permission': ['write:notes', 'read:reactions'],
'callbackUrl': callback
})
r.raise_for_status()
app.client_secret = r.json()['secret']
2021-11-09 10:07:56 +01:00
return app
2021-11-10 07:51:38 +01:00
def login_url(app, callback, session):
# will use the callback we gave the server in `get_or_create_app`
r = session.post('{}://{}/api/auth/session/generate'.format(app.protocol, app.instance), json = {
'appSecret': app.client_secret
})
r.raise_for_status()
# we already get the retrieval token here, but we get it again later so
# we do not have to store it
return r.json()['url']
2021-11-09 10:07:56 +01:00
def receive_token(token, app):
2021-11-10 07:51:38 +01:00
session = make_session()
r = session.post('{}://{}/api/auth/session/userkey'.format(app.protocol, app.instance), json = {
'appSecret': app.client_secret,
'token': token
})
r.raise_for_status()
token = sha256(r.json()['accessToken'].encode('utf-8') + app.client_secret.encode('utf-8')).hexdigest()
2021-11-10 12:33:55 +01:00
acc = account_from_user(r.json()['user'], app.instance)
2021-11-09 10:07:56 +01:00
acc = db.session.merge(acc)
token = OAuthToken(token = token)
token = db.session.merge(token)
token.account = acc
return token
2021-11-10 07:51:38 +01:00
def check_auth(account, app, session):
2021-11-10 12:33:55 +01:00
# there is no explicit check, we can only try getting user info
r = session.post('{}://{}/api/i'.format(app.protocol, app.instance), json = {'i': account.tokens[0].token})
if r.status_code != 200:
raise TemporaryError("{} {}".format(r.status_code, r.text))
if r.json()['isSuspended']:
# this is technically a temporary error, but like for twitter
# its handled as permanent to not make useless API calls
raise PermanentError("Misskey account suspended")
def account_from_user(user, host):
2021-11-09 10:07:56 +01:00
return Account(
2021-11-10 12:33:55 +01:00
# in objects that get returned from misskey, the local host is
# set to None
misskey_instance=host,
2021-11-09 10:07:56 +01:00
misskey_id=user['id'],
2021-11-10 12:33:55 +01:00
screen_name='{}@{}'.format(user['username'], host),
display_name=user['name'],
avatar_url=user['avatarUrl'],
# the notes count is not always included, especially not when
# fetching posts. in that case assume its not needed
reported_post_count=user.get('notesCount', None),
2021-11-09 10:07:56 +01:00
)
2021-11-10 12:33:55 +01:00
def post_from_api_object(obj, host):
2021-11-09 10:07:56 +01:00
return Post(
2021-11-10 12:33:55 +01:00
# in objects that get returned from misskey, the local host is
# set to None
misskey_instance=host,
misskey_id=obj['id'],
favourite=('myReaction' in obj
and bool(obj['myReaction'])),
2021-11-09 10:07:56 +01:00
has_media=('fileIds' in obj
and bool(obj['fileIds'])),
created_at=obj['createdAt'],
2021-11-10 12:33:55 +01:00
author_id=account_from_user(obj['user'], host).id,
2021-11-09 10:07:56 +01:00
direct=obj['visibility'] == 'specified',
is_reblog=obj['renoteId'] is not None,
)
def fetch_posts(acc, max_id, since_id):
app = MisskeyApp.query.get(acc.misskey_instance)
2021-11-10 07:51:38 +01:00
session = make_session()
check_auth(acc, app, session)
2021-11-09 10:07:56 +01:00
2021-11-10 12:33:55 +01:00
kwargs = dict(
2022-02-27 00:25:25 +01:00
limit=100,
2021-11-10 12:33:55 +01:00
userId=acc.misskey_id,
# for some reason the token is needed so misskey also sends `myReaction`
i=acc.tokens[0].token
)
if max_id:
kwargs['untilId'] = max_id
if since_id:
kwargs['sinceId'] = since_id
notes = session.post('{}://{}/api/users/notes'.format(app.protocol, app.instance), json=kwargs)
2021-11-09 10:07:56 +01:00
2021-11-10 12:33:55 +01:00
if notes.status_code != 200:
raise TemporaryError('{} {}'.format(notes.status_code, notes.text))
2021-11-09 10:07:56 +01:00
2021-11-10 12:33:55 +01:00
return [post_from_api_object(note, app.instance) for note in notes.json()]
2021-11-09 10:07:56 +01:00
def refresh_posts(posts):
acc = posts[0].author
app = MisskeyApp.query.get(acc.misskey_instance)
2021-11-10 07:51:38 +01:00
session = make_session()
check_auth(acc, app, session)
2021-11-09 10:07:56 +01:00
new_posts = list()
with db.session.no_autoflush:
for post in posts:
print('Refreshing {}'.format(post))
2021-11-10 12:33:55 +01:00
r = session.post('{}://{}/api/notes/show'.format(app.protocol, app.instance), json={
'i': acc.tokens[0].token,
2021-11-09 10:07:56 +01:00
'noteId': post.misskey_id
})
if r.status_code != 200:
try:
if r.json()['error']['code'] == 'NO_SUCH_NOTE':
db.session.delete(post)
continue
except Exception as e:
raise TemporaryError(e)
2021-11-10 12:33:55 +01:00
raise TemporaryError('{} {}'.format(r.status_code, r.text))
2021-11-09 10:07:56 +01:00
2021-11-10 12:33:55 +01:00
new_post = db.session.merge(post_from_api_object(r.json(), app.instance))
2021-11-09 10:07:56 +01:00
new_post.touch()
new_posts.append(new_post)
return new_posts
def delete(post):
2021-11-10 12:33:55 +01:00
acc = post.author
2021-11-09 10:07:56 +01:00
app = MisskeyApp.query.get(post.misskey_instance)
2021-11-10 07:51:38 +01:00
session = make_session()
2021-11-09 10:07:56 +01:00
if not app:
# how? if this happens, it doesnt make sense to repeat it,
# so use a permanent error
raise PermanentError("instance not registered for delete")
2021-11-10 12:33:55 +01:00
r = session.post('{}://{}/api/notes/delete'.format(app.protocol, app.instance), json = {
'i': acc.tokens[0].token,
2021-11-09 10:07:56 +01:00
'noteId': post.misskey_id
})
if r.status_code != 204:
2021-11-10 12:33:55 +01:00
raise TemporaryError("{} {}".format(r.status_code, r.text))
2021-11-09 10:07:56 +01:00
db.session.delete(post)
def suggested_instances(limit=5, min_popularity=5, blocklist=tuple()):
return tuple((ins.instance for ins in (
MisskeyInstance.query
.filter(MisskeyInstance.popularity > min_popularity)
.filter(~MisskeyInstance.instance.in_(blocklist))
.order_by(db.desc(MisskeyInstance.popularity),
MisskeyInstance.instance)
.limit(limit).all())))