Merge branch 'misskey'
This commit is contained in:
commit
9d2147e905
|
@ -1,5 +1,7 @@
|
|||
## vNEXT
|
||||
|
||||
* add: Misskey support (Thanks @Johann150 !)
|
||||
<https://github.com/codl/forget/pull/544>
|
||||
* fix: lowered database impact of a background task
|
||||
<https://github.com/codl/forget/issue/166>
|
||||
* fix: wording on "favourited posts" is unclear
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
[![Test coverage](https://img.shields.io/codecov/c/github/codl/forget.svg)](https://codecov.io/gh/codl/forget)
|
||||
[![Code quality](https://img.shields.io/codacy/grade/1780ac6071c04cbd9ccf75de0891e798.svg)](https://www.codacy.com/app/codl/forget?utm_source=github.com&utm_medium=referral&utm_content=codl/forget&utm_campaign=badger)
|
||||
|
||||
Forget is a post deleting service for Twitter and Mastodon. It lives at
|
||||
<https://forget.codl.fr>.
|
||||
Forget is a post deleting service for Twitter, Mastodon, and Misskey. It lives
|
||||
at <https://forget.codl.fr>.
|
||||
|
||||
|
||||
## Running your own
|
||||
|
|
|
@ -2,14 +2,23 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
|||
|
||||
(function instance_buttons(){
|
||||
|
||||
const container = document.querySelector('#mastodon_instance_buttons');
|
||||
const button_template = Function('first', 'instance',
|
||||
'return `' + document.querySelector('#instance_button_template').innerHTML + '`;');
|
||||
const another_button_template = Function(
|
||||
const mastodon_container = document.querySelector('#mastodon_instance_buttons');
|
||||
const mastodon_button_template = Function('first', 'instance',
|
||||
'return `' + document.querySelector('#mastodon_instance_button_template').innerHTML + '`;');
|
||||
const mastodon_another_button_template = Function(
|
||||
'return `' +
|
||||
document.querySelector('#another_instance_button_template').innerHTML + '`;');
|
||||
const top_instances =
|
||||
Function('return JSON.parse(`' + document.querySelector('#top_instances').innerHTML + '`);')();
|
||||
document.querySelector('#mastodon_another_instance_button_template').innerHTML + '`;');
|
||||
const mastodon_top_instances =
|
||||
Function('return JSON.parse(`' + document.querySelector('#mastodon_top_instances').innerHTML + '`);')();
|
||||
|
||||
const misskey_container = document.querySelector('#misskey_instance_buttons');
|
||||
const misskey_button_template = Function('first', 'instance',
|
||||
'return `' + document.querySelector('#misskey_instance_button_template').innerHTML + '`;');
|
||||
const misskey_another_button_template = Function(
|
||||
'return `' +
|
||||
document.querySelector('#misskey_another_instance_button_template').innerHTML + '`;');
|
||||
const misskey_top_instances =
|
||||
Function('return JSON.parse(`' + document.querySelector('#misskey_top_instances').innerHTML + '`);')();
|
||||
|
||||
async function get_known(){
|
||||
let known = known_load();
|
||||
|
@ -19,10 +28,16 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
|||
known = await resp.json();
|
||||
}
|
||||
else {
|
||||
known = [{
|
||||
"instance": "mastodon.social",
|
||||
"hits": 0
|
||||
}];
|
||||
known = {
|
||||
mastodon:[{
|
||||
"instance": "mastodon.social",
|
||||
"hits": 0
|
||||
}],
|
||||
misskey:[{
|
||||
"instance": "misskey.io",
|
||||
"hits": 0
|
||||
}],
|
||||
};
|
||||
}
|
||||
known_save(known)
|
||||
fetch('/api/known_instances', {method: 'DELETE'})
|
||||
|
@ -31,17 +46,12 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
|||
return known;
|
||||
}
|
||||
|
||||
|
||||
async function replace_buttons(){
|
||||
let known = await get_known();
|
||||
|
||||
known = normalize_known(known);
|
||||
known_save(known);
|
||||
|
||||
function replace_buttons(top_instances, known_instances, container,
|
||||
template, template_another_instance){
|
||||
let filtered_top_instances = []
|
||||
for(let instance of top_instances){
|
||||
let found = false;
|
||||
for(let k of known){
|
||||
for(let k of known_instances){
|
||||
if(k['instance'] == instance['instance']){
|
||||
found = true;
|
||||
break;
|
||||
|
@ -52,20 +62,35 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
|||
}
|
||||
}
|
||||
|
||||
let instances = known.concat(filtered_top_instances).slice(0, SLOTS);
|
||||
let instances = known_instances.concat(filtered_top_instances).slice(0, SLOTS);
|
||||
|
||||
let html = '';
|
||||
|
||||
let first = true;
|
||||
for(let instance of instances){
|
||||
html += button_template(first, instance['instance'])
|
||||
html += template(first, instance['instance'])
|
||||
first = false;
|
||||
}
|
||||
|
||||
html += another_button_template();
|
||||
html += template_another_instance();
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
replace_buttons();
|
||||
async function init_buttons(){
|
||||
let known = await get_known();
|
||||
|
||||
known.mastodon = normalize_known(known.mastodon);
|
||||
known.misskey = normalize_known(known.misskey);
|
||||
known_save(known);
|
||||
|
||||
replace_buttons(mastodon_top_instances, known.mastodon,
|
||||
mastodon_container, mastodon_button_template,
|
||||
mastodon_another_button_template);
|
||||
replace_buttons(misskey_top_instances, known.misskey,
|
||||
misskey_container, misskey_button_template,
|
||||
misskey_another_button_template);
|
||||
}
|
||||
|
||||
init_buttons();
|
||||
})();
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
const STORAGE_KEY = 'forget_known_instances';
|
||||
const STORAGE_KEY = 'forget_known_instances@2021-12-09';
|
||||
export const SLOTS = 5;
|
||||
|
||||
function load_and_migrate_old(){
|
||||
const OLD_KEY = "forget_known_instances";
|
||||
let olddata = localStorage.getItem(OLD_KEY);
|
||||
if(olddata != null){
|
||||
olddata = JSON.parse(olddata)
|
||||
let newdata = {
|
||||
mastodon: olddata,
|
||||
misskey: [{
|
||||
"instance": "misskey.io",
|
||||
"hits": 0
|
||||
}]
|
||||
};
|
||||
known_save(newdata);
|
||||
localStorage.removeItem(OLD_KEY);
|
||||
return newdata;
|
||||
}
|
||||
}
|
||||
|
||||
export function known_save(known){
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(known));
|
||||
}
|
||||
|
@ -9,6 +27,8 @@ export function known_load(){
|
|||
let known = localStorage.getItem(STORAGE_KEY);
|
||||
if(known){
|
||||
known = JSON.parse(known);
|
||||
} else {
|
||||
known = load_and_migrate_old();
|
||||
}
|
||||
return known;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -194,10 +194,10 @@ import {known_load, known_save} from './known_instances.js'
|
|||
})
|
||||
}
|
||||
|
||||
function bump_instance(instance_name){
|
||||
function bump_instance(service, instance_name){
|
||||
let known_instances = known_load();
|
||||
let found = false;
|
||||
for(let instance of known_instances){
|
||||
for(let instance of known_instances[service]){
|
||||
if(instance['instance'] == instance_name){
|
||||
instance.hits ++;
|
||||
found = true;
|
||||
|
@ -206,15 +206,17 @@ import {known_load, known_save} from './known_instances.js'
|
|||
}
|
||||
if(!found){
|
||||
let instance = {"instance": instance_name, "hits": 1};
|
||||
known_instances.push(instance);
|
||||
known_instances[service].push(instance);
|
||||
}
|
||||
|
||||
known_save(known_instances);
|
||||
|
||||
}
|
||||
|
||||
if(viewer_from_dom['service'] == 'mastodon' && location.hash == '#bump_instance'){
|
||||
bump_instance(viewer_from_dom['id'].split('@')[1])
|
||||
if(location.hash == '#bump_instance' && (
|
||||
viewer_from_dom['service'] == 'mastodon' || viewer_from_dom['service'] == 'misskey'
|
||||
)){
|
||||
bump_instance(viewer_from_dom['service'], viewer_from_dom['id'].split('@')[1])
|
||||
let url = new URL(location.href)
|
||||
url.hash = '';
|
||||
history.replaceState('', '', url);
|
||||
|
|
|
@ -235,6 +235,10 @@ button {
|
|||
background-color: #282c37;
|
||||
}
|
||||
|
||||
.btn.primary.misskey-colored {
|
||||
background-color: #66b300;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.3);
|
||||
|
|
2
dodo.py
2
dodo.py
|
@ -60,7 +60,7 @@ def task_service_icon():
|
|||
formats = ('webp', 'png')
|
||||
for width in widths:
|
||||
for image_format in formats:
|
||||
for basename in ('twitter', 'mastodon'):
|
||||
for basename in ('twitter', 'mastodon', 'misskey'):
|
||||
yield dict(
|
||||
name='{}-{}.{}'.format(basename, width, image_format),
|
||||
actions=[(resize_image, (basename, width, image_format))],
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
from app import db, sentry
|
||||
from model import MisskeyApp, MisskeyInstance, Account, OAuthToken, Post
|
||||
from uuid import uuid4
|
||||
from hashlib import sha256
|
||||
from libforget.exceptions import TemporaryError, PermanentError
|
||||
from libforget.session import make_session
|
||||
|
||||
def get_or_create_app(instance_url, callback, website, session):
|
||||
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 = session.post('https://{}/api/meta'.format(instance_url))
|
||||
r.raise_for_status()
|
||||
proto = 'https'
|
||||
except Exception:
|
||||
r = session.post('http://{}/api/meta'.format(instance_url))
|
||||
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.
|
||||
|
||||
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,
|
||||
'permission': ['read:favorites', 'write:notes'],
|
||||
'callbackUrl': callback
|
||||
})
|
||||
r.raise_for_status()
|
||||
app.client_secret = r.json()['secret']
|
||||
|
||||
return app
|
||||
|
||||
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']
|
||||
|
||||
def receive_token(token, app):
|
||||
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()
|
||||
|
||||
acc = account_from_user(r.json()['user'], app.instance)
|
||||
acc = db.session.merge(acc)
|
||||
token = OAuthToken(token = token)
|
||||
token = db.session.merge(token)
|
||||
token.account = acc
|
||||
|
||||
return token
|
||||
|
||||
def check_auth(account, app, session):
|
||||
# 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):
|
||||
return Account(
|
||||
# in objects that get returned from misskey, the local host is
|
||||
# set to None
|
||||
misskey_instance=host,
|
||||
misskey_id=user['id'],
|
||||
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),
|
||||
)
|
||||
|
||||
def post_from_api_object(obj, host):
|
||||
return Post(
|
||||
# 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'])),
|
||||
has_media=('fileIds' in obj
|
||||
and bool(obj['fileIds'])),
|
||||
created_at=obj['createdAt'],
|
||||
author_id=account_from_user(obj['user'], host).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)
|
||||
session = make_session()
|
||||
check_auth(acc, app, session)
|
||||
|
||||
kwargs = dict(
|
||||
limit=100,
|
||||
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)
|
||||
|
||||
if notes.status_code != 200:
|
||||
raise TemporaryError('{} {}'.format(notes.status_code, notes.text))
|
||||
|
||||
return [post_from_api_object(note, app.instance) for note in notes.json()]
|
||||
|
||||
|
||||
def refresh_posts(posts):
|
||||
acc = posts[0].author
|
||||
app = MisskeyApp.query.get(acc.misskey_instance)
|
||||
session = make_session()
|
||||
check_auth(acc, app, session)
|
||||
|
||||
new_posts = list()
|
||||
with db.session.no_autoflush:
|
||||
for post in posts:
|
||||
print('Refreshing {}'.format(post))
|
||||
r = session.post('{}://{}/api/notes/show'.format(app.protocol, app.instance), json={
|
||||
'i': acc.tokens[0].token,
|
||||
'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.text))
|
||||
|
||||
new_post = db.session.merge(post_from_api_object(r.json(), app.instance))
|
||||
new_post.touch()
|
||||
new_posts.append(new_post)
|
||||
return new_posts
|
||||
|
||||
def delete(post):
|
||||
acc = post.author
|
||||
app = MisskeyApp.query.get(post.misskey_instance)
|
||||
session = make_session()
|
||||
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 = session.post('{}://{}/api/notes/delete'.format(app.protocol, app.instance), json = {
|
||||
'i': acc.tokens[0].token,
|
||||
'noteId': post.misskey_id
|
||||
})
|
||||
|
||||
if r.status_code != 204:
|
||||
raise TemporaryError("{} {}".format(r.status_code, r.text))
|
||||
|
||||
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())))
|
|
@ -0,0 +1,48 @@
|
|||
"""add misskey
|
||||
|
||||
Revision ID: 740fe24a7712
|
||||
Revises: af763dccc0b4
|
||||
Create Date: 2021-11-10 00:13:37.344364
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '740fe24a7712'
|
||||
down_revision = 'af763dccc0b4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('misskey_instances',
|
||||
sa.Column('instance', sa.String(), nullable=False),
|
||||
sa.Column('popularity', sa.Float(), server_default='10', nullable=False),
|
||||
sa.PrimaryKeyConstraint('instance', name=op.f('pk_misskey_instances'))
|
||||
)
|
||||
op.execute("""
|
||||
INSERT INTO misskey_instances (instance, popularity) VALUES
|
||||
('misskey.io', 100),
|
||||
('cliq.social', 60),
|
||||
('misskey.dev', 50),
|
||||
('quietplace.xyz', 40),
|
||||
('mk.nixnet.social', 30),
|
||||
('jigglypuff.club', 20);
|
||||
""")
|
||||
|
||||
op.create_table('misskey_apps',
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('instance', sa.String(), nullable=False),
|
||||
sa.Column('client_secret', sa.String(), nullable=False),
|
||||
sa.Column('protocol', sa.Enum('http', 'https', name='enum_protocol_misskey'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('instance', name=op.f('pk_misskey_apps'))
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('misskey_instances')
|
||||
op.drop_table('misskey_apps')
|
||||
op.execute('DROP TYPE enum_protocol_misskey;')
|
51
model.py
51
model.py
|
@ -67,6 +67,34 @@ class RemoteIDMixin(object):
|
|||
@mastodon_id.setter
|
||||
def mastodon_id(self, id_):
|
||||
self.id = "mastodon:{}@{}".format(id_, self.mastodon_instance)
|
||||
|
||||
@property
|
||||
def misskey_instance(self):
|
||||
if not self.id:
|
||||
return None
|
||||
if self.service != "misskey":
|
||||
raise Exception(
|
||||
"tried to get misskey instance for a {} {}"
|
||||
.format(self.service, type(self)))
|
||||
return self.id.split(":", 1)[1].split('@')[1]
|
||||
|
||||
@misskey_instance.setter
|
||||
def misskey_instance(self, instance):
|
||||
self.id = "misskey:{}@{}".format(self.misskey_id, instance)
|
||||
|
||||
@property
|
||||
def misskey_id(self):
|
||||
if not self.id:
|
||||
return None
|
||||
if self.service != "misskey":
|
||||
raise Exception(
|
||||
"tried to get misskey id for a {} {}"
|
||||
.format(self.service, type(self)))
|
||||
return self.id.split(":", 1)[1].split('@')[0]
|
||||
|
||||
@misskey_id.setter
|
||||
def misskey_id(self, id_):
|
||||
self.id = "misskey:{}@{}".format(id_, self.misskey_instance)
|
||||
|
||||
@property
|
||||
def remote_id(self):
|
||||
|
@ -74,6 +102,8 @@ class RemoteIDMixin(object):
|
|||
return self.twitter_id
|
||||
elif self.service == 'mastodon':
|
||||
return self.mastodon_id
|
||||
elif self.service == 'misskey':
|
||||
return self.misskey_id
|
||||
|
||||
|
||||
ThreeWayPolicyEnum = db.Enum('keeponly', 'deleteonly', 'none',
|
||||
|
@ -364,3 +394,24 @@ class MastodonInstance(db.Model):
|
|||
|
||||
def bump(self, value=1):
|
||||
self.popularity = (self.popularity or 10) + value
|
||||
|
||||
class MisskeyApp(db.Model, TimestampMixin):
|
||||
__tablename__ = 'misskey_apps'
|
||||
|
||||
instance = db.Column(db.String, primary_key=True)
|
||||
protocol = db.Column(db.String, nullable=False)
|
||||
client_secret = db.Column(db.String, nullable=False)
|
||||
|
||||
class MisskeyInstance(db.Model):
|
||||
"""
|
||||
this is for the autocomplete in the misskey login form
|
||||
it isn't coupled with anything else so that we can seed it with
|
||||
some popular instances ahead of time
|
||||
"""
|
||||
__tablename__ = 'misskey_instances'
|
||||
|
||||
instance = db.Column(db.String, primary_key=True)
|
||||
popularity = db.Column(db.Float, server_default='10', nullable=False)
|
||||
|
||||
def bump(self, value=1):
|
||||
self.popularity = (self.popularity or 10) + value
|
||||
|
|
|
@ -3,13 +3,16 @@ from flask import render_template, url_for, redirect, request, g,\
|
|||
from datetime import datetime, timedelta, timezone
|
||||
import libforget.twitter
|
||||
import libforget.mastodon
|
||||
import libforget.misskey
|
||||
from libforget.auth import require_auth, csrf,\
|
||||
get_viewer
|
||||
from model import Session, TwitterArchive, MastodonApp
|
||||
from libforget.session import make_session
|
||||
from model import Session, TwitterArchive, MastodonApp, MisskeyApp
|
||||
from app import app, db, sentry, imgproxy
|
||||
import tasks
|
||||
from zipfile import BadZipFile
|
||||
from twitter import TwitterError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.error import URLError
|
||||
import libforget.version
|
||||
import libforget.settings
|
||||
|
@ -34,10 +37,12 @@ def index():
|
|||
|
||||
@app.route('/about/')
|
||||
def about():
|
||||
instances = libforget.mastodon.suggested_instances()
|
||||
mastodon_instances = libforget.mastodon.suggested_instances()
|
||||
misskey_instances = libforget.misskey.suggested_instances()
|
||||
return render_template(
|
||||
'about.html',
|
||||
mastodon_instances=instances,
|
||||
mastodon_instances=mastodon_instances,
|
||||
misskey_instances=misskey_instances,
|
||||
twitter_login_error='twitter_login_error' in request.args)
|
||||
|
||||
|
||||
|
@ -171,6 +176,9 @@ def logout():
|
|||
return redirect(url_for('about'))
|
||||
|
||||
|
||||
def domain_from_url(url):
|
||||
return urlparse(url).netloc.lower() or urlparse("//"+url).netloc.lower()
|
||||
|
||||
@app.route('/login/mastodon', methods=('GET', 'POST'))
|
||||
def mastodon_login_step1(instance=None):
|
||||
|
||||
|
@ -188,14 +196,7 @@ def mastodon_login_step1(instance=None):
|
|||
generic_error='error' in request.args
|
||||
)
|
||||
|
||||
instance_url = instance_url.lower()
|
||||
# strip protocol
|
||||
instance_url = re.sub('^https?://', '', instance_url,
|
||||
count=1, flags=re.IGNORECASE)
|
||||
# strip username
|
||||
instance_url = instance_url.split("@")[-1]
|
||||
# strip trailing path
|
||||
instance_url = instance_url.split('/')[0]
|
||||
instance_url = domain_from_url(instance_url)
|
||||
|
||||
callback = url_for('mastodon_login_step2',
|
||||
instance_url=instance_url, _external=True)
|
||||
|
@ -240,6 +241,67 @@ def mastodon_login_step2(instance_url):
|
|||
return resp
|
||||
|
||||
|
||||
@app.route('/login/misskey', methods=('GET', 'POST'))
|
||||
def misskey_login(instance=None):
|
||||
instance_url = (request.args.get('instance_url', None)
|
||||
or request.form.get('instance_url', None))
|
||||
|
||||
if not instance_url:
|
||||
instances = libforget.misskey.suggested_instances(
|
||||
limit = 30,
|
||||
min_popularity = 1
|
||||
)
|
||||
return render_template(
|
||||
'misskey_login.html', instances=instances,
|
||||
address_error=request.method == 'POST',
|
||||
generic_error='error' in request.args
|
||||
)
|
||||
|
||||
instance_url = domain_from_url(instance_url)
|
||||
|
||||
callback = url_for('misskey_callback',
|
||||
instance_url=instance_url, _external=True)
|
||||
|
||||
try:
|
||||
session = make_session()
|
||||
app = libforget.misskey.get_or_create_app(
|
||||
instance_url,
|
||||
callback,
|
||||
url_for('index', _external=True),
|
||||
session)
|
||||
db.session.merge(app)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(libforget.misskey.login_url(app, callback, session))
|
||||
|
||||
except Exception:
|
||||
if sentry:
|
||||
sentry.captureException()
|
||||
return redirect(url_for('misskey_login', error=True))
|
||||
|
||||
|
||||
@app.route('/login/misskey/callback/<instance_url>')
|
||||
def misskey_callback(instance_url):
|
||||
# legacy auth and miauth use different parameter names
|
||||
token = request.args.get('token', None) or request.args.get('session', None)
|
||||
app = MisskeyApp.query.get(instance_url)
|
||||
if not token or not app:
|
||||
return redirect(url_for('misskey_login', error=True))
|
||||
|
||||
token = libforget.misskey.receive_token(token, app)
|
||||
account = token.account
|
||||
|
||||
session = login(account.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
g.viewer = session
|
||||
|
||||
resp = redirect(url_for('index', _anchor='bump_instance'))
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/sentry/setup.js')
|
||||
def sentry_setup():
|
||||
client_dsn = app.config.get('SENTRY_DSN').split('@')
|
||||
|
|
39
tasks.py
39
tasks.py
|
@ -2,9 +2,10 @@ from celery import Celery, Task
|
|||
from app import app as flaskapp
|
||||
from app import db
|
||||
from model import Session, Account, TwitterArchive, Post, OAuthToken,\
|
||||
MastodonInstance
|
||||
MastodonInstance, MisskeyInstance
|
||||
import libforget.twitter
|
||||
import libforget.mastodon
|
||||
import libforget.misskey
|
||||
from datetime import timedelta, datetime, timezone
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
|
@ -151,6 +152,8 @@ def fetch_acc(id_):
|
|||
fetch_posts = libforget.twitter.fetch_posts
|
||||
elif (account.service == 'mastodon'):
|
||||
fetch_posts = libforget.mastodon.fetch_posts
|
||||
elif (account.service == 'misskey'):
|
||||
fetch_posts = libforget.misskey.fetch_posts
|
||||
posts = fetch_posts(account, max_id, since_id)
|
||||
|
||||
if posts is None:
|
||||
|
@ -291,6 +294,10 @@ def delete_from_account(account_id):
|
|||
if refreshed and is_eligible(refreshed[0]):
|
||||
to_delete = refreshed[0]
|
||||
break
|
||||
elif account.service == 'misskey':
|
||||
action = libforget.misskey.delete
|
||||
posts = refresh_posts(posts)
|
||||
to_delete = next(filter(is_eligible, posts), None)
|
||||
|
||||
if to_delete:
|
||||
print("Deleting {}".format(to_delete))
|
||||
|
@ -317,6 +324,8 @@ def refresh_posts(posts):
|
|||
return libforget.twitter.refresh_posts(posts)
|
||||
elif posts[0].service == 'mastodon':
|
||||
return libforget.mastodon.refresh_posts(posts)
|
||||
elif posts[0].service == 'misskey':
|
||||
return libforget.misskey.refresh_posts(posts)
|
||||
|
||||
|
||||
@app.task()
|
||||
|
@ -474,6 +483,33 @@ def update_mastodon_instances_popularity():
|
|||
})
|
||||
db.session.commit()
|
||||
|
||||
@app.task
|
||||
def update_misskey_instances_popularity():
|
||||
# bump score for each active account
|
||||
for acct in (Account.query.options(db.joinedload(Account.sessions))
|
||||
.filter(~Account.dormant).filter(
|
||||
Account.id.like('misskey:%'))):
|
||||
instance = MisskeyInstance.query.get(acct.misskey_instance)
|
||||
if not instance:
|
||||
instance = MisskeyInstance(
|
||||
instance=acct.misskey_instance, popularity=10)
|
||||
db.session.add(instance)
|
||||
amount = 0.01
|
||||
if acct.policy_enabled:
|
||||
amount = 0.5
|
||||
for _ in acct.sessions:
|
||||
amount += 0.1
|
||||
instance.bump(amount / max(1, instance.popularity))
|
||||
|
||||
# normalise scores so the top is 20
|
||||
top_pop = (db.session.query(db.func.max(MisskeyInstance.popularity))
|
||||
.scalar())
|
||||
MisskeyInstance.query.update({
|
||||
MisskeyInstance.popularity:
|
||||
MisskeyInstance.popularity * 20 / top_pop
|
||||
})
|
||||
db.session.commit()
|
||||
|
||||
|
||||
app.add_periodic_task(40, queue_fetch_for_most_stale_accounts)
|
||||
app.add_periodic_task(9, queue_deletes)
|
||||
|
@ -481,6 +517,7 @@ app.add_periodic_task(6, refresh_account_with_oldest_post)
|
|||
app.add_periodic_task(50, refresh_account_with_longest_time_since_refresh)
|
||||
app.add_periodic_task(300, periodic_cleanup)
|
||||
app.add_periodic_task(300, update_mastodon_instances_popularity)
|
||||
app.add_periodic_task(300, update_misskey_instances_popularity)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.worker_main()
|
||||
|
|
|
@ -46,11 +46,37 @@
|
|||
</a>
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
|
||||
<p id='misskey_instance_buttons'>
|
||||
{% for instance in misskey_instances %}
|
||||
<a class='btn primary misskey-colored' href="{{ url_for('misskey_login', instance_url=instance) }}">
|
||||
{% if loop.first %}
|
||||
{{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}}
|
||||
Log in with
|
||||
{% endif %}
|
||||
{{instance}}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class='btn primary misskey-colored' href="{{ url_for('misskey_login') }}">
|
||||
{{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}}
|
||||
Log in with Misskey
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% if misskey_instances %}
|
||||
<a class='btn secondary' href="{{ url_for('misskey_login') }}">
|
||||
Another Misskey instance
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Mastodon -->
|
||||
|
||||
<script type="application/json" id="top_instances">
|
||||
<script type="application/json" id="mastodon_top_instances">
|
||||
[
|
||||
{% for instance in mastodon_instances %}
|
||||
{"instance": "{{instance}}"}
|
||||
|
@ -61,7 +87,7 @@
|
|||
]
|
||||
</script>
|
||||
|
||||
<script type="text/html+template" id="instance_button_template">
|
||||
<script type="text/html+template" id="mastodon_instance_button_template">
|
||||
<a class='btn primary mastodon-colored'
|
||||
href="{{ url_for('mastodon_login_step1') }}?instance_url=${encodeURIComponent(instance)}">
|
||||
${ !first? '' : `
|
||||
|
@ -72,12 +98,43 @@
|
|||
</a>
|
||||
</script>
|
||||
|
||||
<script type="text/html+template" id="another_instance_button_template">
|
||||
<script type="text/html+template" id="mastodon_another_instance_button_template">
|
||||
<a class='btn secondary' href="{{ url_for('mastodon_login_step1') }}">
|
||||
Another Mastodon instance
|
||||
</a>
|
||||
</script>
|
||||
|
||||
<!-- Misskey -->
|
||||
|
||||
<script type="application/json" id="misskey_top_instances">
|
||||
[
|
||||
{% for instance in misskey_instances %}
|
||||
{"instance": "{{instance}}"}
|
||||
{%- if not loop.last -%}
|
||||
,
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
</script>
|
||||
|
||||
<script type="text/html+template" id="misskey_instance_button_template">
|
||||
<a class='btn primary misskey-colored'
|
||||
href="{{ url_for('misskey_login') }}?instance_url=${encodeURIComponent(instance)}">
|
||||
${ !first? '' : `
|
||||
{{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}}
|
||||
Log in with
|
||||
`}
|
||||
${ instance }
|
||||
</a>
|
||||
</script>
|
||||
|
||||
<script type="text/html+template" id="misskey_another_instance_button_template">
|
||||
<a class='btn secondary' href="{{ url_for('misskey_login') }}">
|
||||
Another Misskey instance
|
||||
</a>
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
@ -86,7 +143,7 @@
|
|||
<ul>
|
||||
<li>Delete your posts when they cross an age threshold.</li>
|
||||
<li>Or keep your post count in check, deleting old posts when you go over.</li>
|
||||
<li>Preserve old posts that matter by giving them a favourite.</li>
|
||||
<li>Preserve old posts that matter by giving them a favourite or a reaction (Misskey only).</li>
|
||||
<li>Set it and <i>forget</i> it. Forget works continuously in the background.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{% extends 'lib/layout.html' %}
|
||||
{% block body %}
|
||||
<section>
|
||||
<h2>Log in with Misskey</h2>
|
||||
|
||||
{% if generic_error %}
|
||||
<div class='banner error'>Something went wrong while logging in. Try again?</div>
|
||||
{% endif %}
|
||||
|
||||
{% if address_error %}
|
||||
<div class='banner error'>This doesn't look like a misskey instance url. Try again?</div>
|
||||
{% endif %}
|
||||
|
||||
<form method='post'>
|
||||
<label>misskey instance:
|
||||
<input type='text' name='instance_url' list='instances' placeholder='social.example.net'/>
|
||||
</label>
|
||||
<datalist id='instances'>
|
||||
<option value=''>
|
||||
{% for instance in instances %}
|
||||
<option value='{{instance}}'>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<input name='confirm' value='Log in' type='submit'/>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -12,12 +12,12 @@
|
|||
<li>A unique post identifier</li>
|
||||
<li>The post's time and date of publishing</li>
|
||||
<li>Whether the post has any media attached</li>
|
||||
<li>Whether the post has been favourited by you</li>
|
||||
<li>(Mastodon only) Whether the post is a direct message</li>
|
||||
<li>Whether the post has been favourited by you (only Twitter or Mastodon); or if (not how) you reacted to the post (Misskey only)</li>
|
||||
<li>Whether the post is a direct message (only Mastodon or Misskey)</li>
|
||||
</ul>
|
||||
<p>No other post metadata and no post contents are stored by Forget.</p>
|
||||
|
||||
<p>Last updated on 2021-05-14. <a href="https://github.com/codl/forget/commits/master/templates/privacy.html">History</a>.</p>
|
||||
<p>Last updated on 2021-11-11. <a href="https://github.com/codl/forget/commits/master/templates/privacy.html">History</a>.</p>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in New Issue