Merge branch 'misskey'
This commit is contained in:
commit
9d2147e905
|
@ -1,5 +1,7 @@
|
||||||
## vNEXT
|
## vNEXT
|
||||||
|
|
||||||
|
* add: Misskey support (Thanks @Johann150 !)
|
||||||
|
<https://github.com/codl/forget/pull/544>
|
||||||
* fix: lowered database impact of a background task
|
* fix: lowered database impact of a background task
|
||||||
<https://github.com/codl/forget/issue/166>
|
<https://github.com/codl/forget/issue/166>
|
||||||
* fix: wording on "favourited posts" is unclear
|
* 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)
|
[![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)
|
[![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
|
Forget is a post deleting service for Twitter, Mastodon, and Misskey. It lives
|
||||||
<https://forget.codl.fr>.
|
at <https://forget.codl.fr>.
|
||||||
|
|
||||||
|
|
||||||
## Running your own
|
## Running your own
|
||||||
|
|
|
@ -2,14 +2,23 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
||||||
|
|
||||||
(function instance_buttons(){
|
(function instance_buttons(){
|
||||||
|
|
||||||
const container = document.querySelector('#mastodon_instance_buttons');
|
const mastodon_container = document.querySelector('#mastodon_instance_buttons');
|
||||||
const button_template = Function('first', 'instance',
|
const mastodon_button_template = Function('first', 'instance',
|
||||||
'return `' + document.querySelector('#instance_button_template').innerHTML + '`;');
|
'return `' + document.querySelector('#mastodon_instance_button_template').innerHTML + '`;');
|
||||||
const another_button_template = Function(
|
const mastodon_another_button_template = Function(
|
||||||
'return `' +
|
'return `' +
|
||||||
document.querySelector('#another_instance_button_template').innerHTML + '`;');
|
document.querySelector('#mastodon_another_instance_button_template').innerHTML + '`;');
|
||||||
const top_instances =
|
const mastodon_top_instances =
|
||||||
Function('return JSON.parse(`' + document.querySelector('#top_instances').innerHTML + '`);')();
|
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(){
|
async function get_known(){
|
||||||
let known = known_load();
|
let known = known_load();
|
||||||
|
@ -19,10 +28,16 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
||||||
known = await resp.json();
|
known = await resp.json();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
known = [{
|
known = {
|
||||||
"instance": "mastodon.social",
|
mastodon:[{
|
||||||
"hits": 0
|
"instance": "mastodon.social",
|
||||||
}];
|
"hits": 0
|
||||||
|
}],
|
||||||
|
misskey:[{
|
||||||
|
"instance": "misskey.io",
|
||||||
|
"hits": 0
|
||||||
|
}],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
known_save(known)
|
known_save(known)
|
||||||
fetch('/api/known_instances', {method: 'DELETE'})
|
fetch('/api/known_instances', {method: 'DELETE'})
|
||||||
|
@ -31,17 +46,12 @@ import {SLOTS, normalize_known, known_load, known_save} from './known_instances.
|
||||||
return known;
|
return known;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replace_buttons(top_instances, known_instances, container,
|
||||||
async function replace_buttons(){
|
template, template_another_instance){
|
||||||
let known = await get_known();
|
|
||||||
|
|
||||||
known = normalize_known(known);
|
|
||||||
known_save(known);
|
|
||||||
|
|
||||||
let filtered_top_instances = []
|
let filtered_top_instances = []
|
||||||
for(let instance of top_instances){
|
for(let instance of top_instances){
|
||||||
let found = false;
|
let found = false;
|
||||||
for(let k of known){
|
for(let k of known_instances){
|
||||||
if(k['instance'] == instance['instance']){
|
if(k['instance'] == instance['instance']){
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
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 html = '';
|
||||||
|
|
||||||
let first = true;
|
let first = true;
|
||||||
for(let instance of instances){
|
for(let instance of instances){
|
||||||
html += button_template(first, instance['instance'])
|
html += template(first, instance['instance'])
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += another_button_template();
|
html += template_another_instance();
|
||||||
|
|
||||||
container.innerHTML = html;
|
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;
|
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){
|
export function known_save(known){
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(known));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(known));
|
||||||
}
|
}
|
||||||
|
@ -9,6 +27,8 @@ export function known_load(){
|
||||||
let known = localStorage.getItem(STORAGE_KEY);
|
let known = localStorage.getItem(STORAGE_KEY);
|
||||||
if(known){
|
if(known){
|
||||||
known = JSON.parse(known);
|
known = JSON.parse(known);
|
||||||
|
} else {
|
||||||
|
known = load_and_migrate_old();
|
||||||
}
|
}
|
||||||
return known;
|
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 known_instances = known_load();
|
||||||
let found = false;
|
let found = false;
|
||||||
for(let instance of known_instances){
|
for(let instance of known_instances[service]){
|
||||||
if(instance['instance'] == instance_name){
|
if(instance['instance'] == instance_name){
|
||||||
instance.hits ++;
|
instance.hits ++;
|
||||||
found = true;
|
found = true;
|
||||||
|
@ -206,15 +206,17 @@ import {known_load, known_save} from './known_instances.js'
|
||||||
}
|
}
|
||||||
if(!found){
|
if(!found){
|
||||||
let instance = {"instance": instance_name, "hits": 1};
|
let instance = {"instance": instance_name, "hits": 1};
|
||||||
known_instances.push(instance);
|
known_instances[service].push(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
known_save(known_instances);
|
known_save(known_instances);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(viewer_from_dom['service'] == 'mastodon' && location.hash == '#bump_instance'){
|
if(location.hash == '#bump_instance' && (
|
||||||
bump_instance(viewer_from_dom['id'].split('@')[1])
|
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)
|
let url = new URL(location.href)
|
||||||
url.hash = '';
|
url.hash = '';
|
||||||
history.replaceState('', '', url);
|
history.replaceState('', '', url);
|
||||||
|
|
|
@ -235,6 +235,10 @@ button {
|
||||||
background-color: #282c37;
|
background-color: #282c37;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.primary.misskey-colored {
|
||||||
|
background-color: #66b300;
|
||||||
|
}
|
||||||
|
|
||||||
.btn.secondary {
|
.btn.secondary {
|
||||||
background-color: rgba(255,255,255,0.5);
|
background-color: rgba(255,255,255,0.5);
|
||||||
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.3);
|
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')
|
formats = ('webp', 'png')
|
||||||
for width in widths:
|
for width in widths:
|
||||||
for image_format in formats:
|
for image_format in formats:
|
||||||
for basename in ('twitter', 'mastodon'):
|
for basename in ('twitter', 'mastodon', 'misskey'):
|
||||||
yield dict(
|
yield dict(
|
||||||
name='{}-{}.{}'.format(basename, width, image_format),
|
name='{}-{}.{}'.format(basename, width, image_format),
|
||||||
actions=[(resize_image, (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
|
@mastodon_id.setter
|
||||||
def mastodon_id(self, id_):
|
def mastodon_id(self, id_):
|
||||||
self.id = "mastodon:{}@{}".format(id_, self.mastodon_instance)
|
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
|
@property
|
||||||
def remote_id(self):
|
def remote_id(self):
|
||||||
|
@ -74,6 +102,8 @@ class RemoteIDMixin(object):
|
||||||
return self.twitter_id
|
return self.twitter_id
|
||||||
elif self.service == 'mastodon':
|
elif self.service == 'mastodon':
|
||||||
return self.mastodon_id
|
return self.mastodon_id
|
||||||
|
elif self.service == 'misskey':
|
||||||
|
return self.misskey_id
|
||||||
|
|
||||||
|
|
||||||
ThreeWayPolicyEnum = db.Enum('keeponly', 'deleteonly', 'none',
|
ThreeWayPolicyEnum = db.Enum('keeponly', 'deleteonly', 'none',
|
||||||
|
@ -364,3 +394,24 @@ class MastodonInstance(db.Model):
|
||||||
|
|
||||||
def bump(self, value=1):
|
def bump(self, value=1):
|
||||||
self.popularity = (self.popularity or 10) + value
|
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
|
from datetime import datetime, timedelta, timezone
|
||||||
import libforget.twitter
|
import libforget.twitter
|
||||||
import libforget.mastodon
|
import libforget.mastodon
|
||||||
|
import libforget.misskey
|
||||||
from libforget.auth import require_auth, csrf,\
|
from libforget.auth import require_auth, csrf,\
|
||||||
get_viewer
|
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
|
from app import app, db, sentry, imgproxy
|
||||||
import tasks
|
import tasks
|
||||||
from zipfile import BadZipFile
|
from zipfile import BadZipFile
|
||||||
from twitter import TwitterError
|
from twitter import TwitterError
|
||||||
|
from urllib.parse import urlparse
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
import libforget.version
|
import libforget.version
|
||||||
import libforget.settings
|
import libforget.settings
|
||||||
|
@ -34,10 +37,12 @@ def index():
|
||||||
|
|
||||||
@app.route('/about/')
|
@app.route('/about/')
|
||||||
def about():
|
def about():
|
||||||
instances = libforget.mastodon.suggested_instances()
|
mastodon_instances = libforget.mastodon.suggested_instances()
|
||||||
|
misskey_instances = libforget.misskey.suggested_instances()
|
||||||
return render_template(
|
return render_template(
|
||||||
'about.html',
|
'about.html',
|
||||||
mastodon_instances=instances,
|
mastodon_instances=mastodon_instances,
|
||||||
|
misskey_instances=misskey_instances,
|
||||||
twitter_login_error='twitter_login_error' in request.args)
|
twitter_login_error='twitter_login_error' in request.args)
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,6 +176,9 @@ def logout():
|
||||||
return redirect(url_for('about'))
|
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'))
|
@app.route('/login/mastodon', methods=('GET', 'POST'))
|
||||||
def mastodon_login_step1(instance=None):
|
def mastodon_login_step1(instance=None):
|
||||||
|
|
||||||
|
@ -188,14 +196,7 @@ def mastodon_login_step1(instance=None):
|
||||||
generic_error='error' in request.args
|
generic_error='error' in request.args
|
||||||
)
|
)
|
||||||
|
|
||||||
instance_url = instance_url.lower()
|
instance_url = domain_from_url(instance_url)
|
||||||
# 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]
|
|
||||||
|
|
||||||
callback = url_for('mastodon_login_step2',
|
callback = url_for('mastodon_login_step2',
|
||||||
instance_url=instance_url, _external=True)
|
instance_url=instance_url, _external=True)
|
||||||
|
@ -240,6 +241,67 @@ def mastodon_login_step2(instance_url):
|
||||||
return resp
|
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')
|
@app.route('/sentry/setup.js')
|
||||||
def sentry_setup():
|
def sentry_setup():
|
||||||
client_dsn = app.config.get('SENTRY_DSN').split('@')
|
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 app as flaskapp
|
||||||
from app import db
|
from app import db
|
||||||
from model import Session, Account, TwitterArchive, Post, OAuthToken,\
|
from model import Session, Account, TwitterArchive, Post, OAuthToken,\
|
||||||
MastodonInstance
|
MastodonInstance, MisskeyInstance
|
||||||
import libforget.twitter
|
import libforget.twitter
|
||||||
import libforget.mastodon
|
import libforget.mastodon
|
||||||
|
import libforget.misskey
|
||||||
from datetime import timedelta, datetime, timezone
|
from datetime import timedelta, datetime, timezone
|
||||||
from time import time
|
from time import time
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
@ -151,6 +152,8 @@ def fetch_acc(id_):
|
||||||
fetch_posts = libforget.twitter.fetch_posts
|
fetch_posts = libforget.twitter.fetch_posts
|
||||||
elif (account.service == 'mastodon'):
|
elif (account.service == 'mastodon'):
|
||||||
fetch_posts = libforget.mastodon.fetch_posts
|
fetch_posts = libforget.mastodon.fetch_posts
|
||||||
|
elif (account.service == 'misskey'):
|
||||||
|
fetch_posts = libforget.misskey.fetch_posts
|
||||||
posts = fetch_posts(account, max_id, since_id)
|
posts = fetch_posts(account, max_id, since_id)
|
||||||
|
|
||||||
if posts is None:
|
if posts is None:
|
||||||
|
@ -291,6 +294,10 @@ def delete_from_account(account_id):
|
||||||
if refreshed and is_eligible(refreshed[0]):
|
if refreshed and is_eligible(refreshed[0]):
|
||||||
to_delete = refreshed[0]
|
to_delete = refreshed[0]
|
||||||
break
|
break
|
||||||
|
elif account.service == 'misskey':
|
||||||
|
action = libforget.misskey.delete
|
||||||
|
posts = refresh_posts(posts)
|
||||||
|
to_delete = next(filter(is_eligible, posts), None)
|
||||||
|
|
||||||
if to_delete:
|
if to_delete:
|
||||||
print("Deleting {}".format(to_delete))
|
print("Deleting {}".format(to_delete))
|
||||||
|
@ -317,6 +324,8 @@ def refresh_posts(posts):
|
||||||
return libforget.twitter.refresh_posts(posts)
|
return libforget.twitter.refresh_posts(posts)
|
||||||
elif posts[0].service == 'mastodon':
|
elif posts[0].service == 'mastodon':
|
||||||
return libforget.mastodon.refresh_posts(posts)
|
return libforget.mastodon.refresh_posts(posts)
|
||||||
|
elif posts[0].service == 'misskey':
|
||||||
|
return libforget.misskey.refresh_posts(posts)
|
||||||
|
|
||||||
|
|
||||||
@app.task()
|
@app.task()
|
||||||
|
@ -474,6 +483,33 @@ def update_mastodon_instances_popularity():
|
||||||
})
|
})
|
||||||
db.session.commit()
|
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(40, queue_fetch_for_most_stale_accounts)
|
||||||
app.add_periodic_task(9, queue_deletes)
|
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(50, refresh_account_with_longest_time_since_refresh)
|
||||||
app.add_periodic_task(300, periodic_cleanup)
|
app.add_periodic_task(300, periodic_cleanup)
|
||||||
app.add_periodic_task(300, update_mastodon_instances_popularity)
|
app.add_periodic_task(300, update_mastodon_instances_popularity)
|
||||||
|
app.add_periodic_task(300, update_misskey_instances_popularity)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.worker_main()
|
app.worker_main()
|
||||||
|
|
|
@ -46,11 +46,37 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Mastodon -->
|
||||||
|
|
||||||
<script type="application/json" id="top_instances">
|
<script type="application/json" id="mastodon_top_instances">
|
||||||
[
|
[
|
||||||
{% for instance in mastodon_instances %}
|
{% for instance in mastodon_instances %}
|
||||||
{"instance": "{{instance}}"}
|
{"instance": "{{instance}}"}
|
||||||
|
@ -61,7 +87,7 @@
|
||||||
]
|
]
|
||||||
</script>
|
</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'
|
<a class='btn primary mastodon-colored'
|
||||||
href="{{ url_for('mastodon_login_step1') }}?instance_url=${encodeURIComponent(instance)}">
|
href="{{ url_for('mastodon_login_step1') }}?instance_url=${encodeURIComponent(instance)}">
|
||||||
${ !first? '' : `
|
${ !first? '' : `
|
||||||
|
@ -72,12 +98,43 @@
|
||||||
</a>
|
</a>
|
||||||
</script>
|
</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') }}">
|
<a class='btn secondary' href="{{ url_for('mastodon_login_step1') }}">
|
||||||
Another Mastodon instance
|
Another Mastodon instance
|
||||||
</a>
|
</a>
|
||||||
</script>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -86,7 +143,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>Delete your posts when they cross an age threshold.</li>
|
<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>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>
|
<li>Set it and <i>forget</i> it. Forget works continuously in the background.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</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>A unique post identifier</li>
|
||||||
<li>The post's time and date of publishing</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 any media attached</li>
|
||||||
<li>Whether the post has been favourited by you</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>(Mastodon only) Whether the post is a direct message</li>
|
<li>Whether the post is a direct message (only Mastodon or Misskey)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>No other post metadata and no post contents are stored by Forget.</p>
|
<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>
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue