211 lines
7.4 KiB
Python
211 lines
7.4 KiB
Python
from app import db, sentry
|
|
from model import MisskeyApp, MisskeyInstance, Account, OAuthToken
|
|
from requests import get, post
|
|
from uuid import uuid4
|
|
from hashlib import sha256
|
|
from libforget.exceptions import TemporaryError, PermanentError
|
|
|
|
def get_or_create_app(instance_url, callback, website):
|
|
instance_url = instance_url
|
|
app = MisskeyApp.query.get(instance_url)
|
|
|
|
if not app:
|
|
# check if the instance uses https while getting instance infos
|
|
try:
|
|
r = post('https://{}/api/meta'.format(instance_url))
|
|
r.raise_for_status()
|
|
proto = 'https'
|
|
except Exception:
|
|
r = post('http://{}/api/meta'.format(instance_url))
|
|
r.raise_for_status()
|
|
proto = 'http'
|
|
|
|
# check if miauth is available or we have to use legacy auth
|
|
miauth = 'miauth' in r.json()['features']
|
|
|
|
app = MisskeyApp()
|
|
app.instance = instance_url
|
|
app.protocol = proto
|
|
app.miauth = miauth
|
|
|
|
if miauth:
|
|
# apps do not have to be registered for miauth
|
|
app.client_secret = None
|
|
else:
|
|
# register the app
|
|
r = post('{}://{}/api/app/create'.format(app.protocol, app.instance), json = {
|
|
'name': 'forget',
|
|
'description': website,
|
|
'permission': ['read:favorites', 'write:notes'],
|
|
'callbackUrl': callback
|
|
})
|
|
r.raise_for_status()
|
|
app.client_secret = r.json()['secret']
|
|
|
|
return app
|
|
|
|
def login_url(app, callback):
|
|
if app.miauth:
|
|
return "{}://{}/miauth/{}?name=forget&callback={}&permission=read:favorites,write:notes".format(app.protocol, app.instance, uuid4(), callback)
|
|
else:
|
|
# will use the callback we gave the server in `get_or_create_app`
|
|
r = 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']
|
|
|
|
def receive_token(token, app):
|
|
if app.miauth:
|
|
r = get('{}://{}/api/miauth/{}/check'.format(app.protocol, app.instance, token))
|
|
r.raise_for_status()
|
|
|
|
token = r.json()['token']
|
|
|
|
acc = account_from_user(r.json()['user'])
|
|
acc = db.session.merge(acc)
|
|
token = OAuthToken(token = r.json()['token'])
|
|
token = db.session.merge(token)
|
|
token.account = acc
|
|
else:
|
|
r = 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()
|
|
|
|
acc = account_from_user(r.json()['user'])
|
|
acc = db.session.merge(acc)
|
|
token = OAuthToken(token = token)
|
|
token = db.session.merge(token)
|
|
token.account = acc
|
|
|
|
return token
|
|
|
|
def check_auth(account, app):
|
|
if app.miauth:
|
|
r = get('{}://{}/api/miauth/{}/check'.format(app.protocol, app.instance, account.token))
|
|
|
|
if r.status_code != 200:
|
|
raise TemporaryError("{} {}".format(r.status_code, r.body))
|
|
|
|
if not r.json()['ok']:
|
|
if sentry:
|
|
sentry.captureMessage(
|
|
'Misskey auth revoked or incorrect',
|
|
extra=locals())
|
|
db.session.delete(token)
|
|
db.session.commit()
|
|
raise PermanentError("Misskey auth revoked")
|
|
else:
|
|
# there is no such check for legacy auth, instead we check if we can
|
|
# get the user info
|
|
r = post('{}://{}/api/i'.format(app.protocol, app.instance), json = {'i': account.token})
|
|
|
|
if r.status_code != 200:
|
|
raise TemporaryError("{} {}".format(r.status_code, r.body))
|
|
|
|
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):
|
|
return Account(
|
|
misskey_instance=user['host'],
|
|
misskey_id=user['id'],
|
|
screen_name='{}@{}'.format(user['username'], user['host']),
|
|
display_name=obj['name'],
|
|
avatar_url=obj['avatarUrl'],
|
|
reported_post_count=obj['notesCount'],
|
|
)
|
|
|
|
def post_from_api_object(obj):
|
|
return Post(
|
|
misskey_instance=user['host'],
|
|
misskey_id=user['id'],
|
|
favourite=obj['myReaction'] is not None,
|
|
has_media=('fileIds' in obj
|
|
and bool(obj['fileIds'])),
|
|
created_at=obj['createdAt'],
|
|
author_id=account_from_user(obj['user']).id,
|
|
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)
|
|
check_auth(acc, app)
|
|
if not verify_credentials(acc, app):
|
|
raise PermanentError()
|
|
try:
|
|
kwargs = dict(limit=40)
|
|
if max_id:
|
|
kwargs['untilId'] = max_id
|
|
if since_id:
|
|
kwargs['sinceId'] = since_id
|
|
|
|
notes = post('{}://{}/api/users/notes'.format(app.protocol, app.misskey_instance), json=kwargs)
|
|
notes.raise_for_status()
|
|
|
|
return [post_from_api_object(status) for note in notes.json()]
|
|
|
|
except Exception as e:
|
|
raise TemporaryError(e)
|
|
|
|
|
|
def refresh_posts(posts):
|
|
acc = posts[0].author
|
|
app = MisskeyApp.query.get(acc.misskey_instance)
|
|
check_auth(acc, app)
|
|
|
|
new_posts = list()
|
|
with db.session.no_autoflush:
|
|
for post in posts:
|
|
print('Refreshing {}'.format(post))
|
|
r = post('{}://{}/api/notes/show'.format(app.protocol, app.misskey_instance), json={
|
|
'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)
|
|
raise TemporaryError('{} {}'.format(r.status_code, r.body))
|
|
|
|
new_post = db.session.merge(post_from_api_object(r.json()))
|
|
new_post.touch()
|
|
new_posts.append(new_post)
|
|
return new_posts
|
|
|
|
def delete(post):
|
|
app = MisskeyApp.query.get(post.misskey_instance)
|
|
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")
|
|
|
|
r = post('{}://{}/api/notes/delete'.format(app.protocol, app.misskey_instance), json = {
|
|
'noteId': post.misskey_id
|
|
})
|
|
|
|
if r.status_code != 204:
|
|
raise TemporaryError("{} {}".format(r.status_code, r.body))
|
|
|
|
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())))
|