diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
index b6fc2c5..98ec7df 100644
--- a/CHANGELOG.markdown
+++ b/CHANGELOG.markdown
@@ -1,5 +1,7 @@
## vNEXT
+* add: Misskey support (Thanks @Johann150 !)
+
* fix: lowered database impact of a background task
* fix: wording on "favourited posts" is unclear
diff --git a/README.markdown b/README.markdown
index cac9ca9..dc7219d 100644
--- a/README.markdown
+++ b/README.markdown
@@ -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
-.
+Forget is a post deleting service for Twitter, Mastodon, and Misskey. It lives
+at .
## Running your own
diff --git a/assets/instance_buttons.js b/assets/instance_buttons.js
index 6c2bfc1..d5bbf53 100644
--- a/assets/instance_buttons.js
+++ b/assets/instance_buttons.js
@@ -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();
})();
diff --git a/assets/known_instances.js b/assets/known_instances.js
index eb1e746..cd6fff6 100644
--- a/assets/known_instances.js
+++ b/assets/known_instances.js
@@ -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;
}
diff --git a/assets/misskey.png b/assets/misskey.png
new file mode 100644
index 0000000..1e57da6
Binary files /dev/null and b/assets/misskey.png differ
diff --git a/assets/settings.js b/assets/settings.js
index 3b400ec..9aad005 100644
--- a/assets/settings.js
+++ b/assets/settings.js
@@ -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);
diff --git a/assets/styles.css b/assets/styles.css
index b9dfc1a..c1dda3a 100644
--- a/assets/styles.css
+++ b/assets/styles.css
@@ -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);
diff --git a/dodo.py b/dodo.py
index 75ceb08..f737ba2 100644
--- a/dodo.py
+++ b/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))],
diff --git a/libforget/misskey.py b/libforget/misskey.py
new file mode 100644
index 0000000..06e3449
--- /dev/null
+++ b/libforget/misskey.py
@@ -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())))
diff --git a/migrations/versions/740fe24a7712_add_misskey.py b/migrations/versions/740fe24a7712_add_misskey.py
new file mode 100644
index 0000000..9e87946
--- /dev/null
+++ b/migrations/versions/740fe24a7712_add_misskey.py
@@ -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;')
diff --git a/model.py b/model.py
index 01cfd29..b65dbc0 100644
--- a/model.py
+++ b/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
diff --git a/routes/__init__.py b/routes/__init__.py
index bfa2976..f665fe6 100644
--- a/routes/__init__.py
+++ b/routes/__init__.py
@@ -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/')
+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('@')
diff --git a/tasks.py b/tasks.py
index 61fc555..d0f594e 100644
--- a/tasks.py
+++ b/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()
diff --git a/templates/about.html b/templates/about.html
index ede9d57..aa6e3c4 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -46,11 +46,37 @@
{% endif %}
+
+
+
+{% for instance in misskey_instances %}
+
+{% if loop.first %}
+ {{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}}
+ Log in with
+{% endif %}
+ {{instance}}
+
+{% else %}
+
+ {{picture(st, 'misskey', (20,40,80), ('webp', 'png'))}}
+ Log in with Misskey
+
+{% endfor %}
+
+
+{% if misskey_instances %}
+
+ Another Misskey instance
+
+{% endif %}
+
+
-
-
-
+
+
+
+
+
+
+
+
+
{% endif %}
@@ -86,7 +143,7 @@
- Delete your posts when they cross an age threshold.
- Or keep your post count in check, deleting old posts when you go over.
- - Preserve old posts that matter by giving them a favourite.
+ - Preserve old posts that matter by giving them a favourite or a reaction (Misskey only).
- Set it and forget it. Forget works continuously in the background.
diff --git a/templates/misskey_login.html b/templates/misskey_login.html
new file mode 100644
index 0000000..c830078
--- /dev/null
+++ b/templates/misskey_login.html
@@ -0,0 +1,29 @@
+{% extends 'lib/layout.html' %}
+{% block body %}
+
+ Log in with Misskey
+
+{% if generic_error %}
+ Something went wrong while logging in. Try again?
+{% endif %}
+
+{% if address_error %}
+ This doesn't look like a misskey instance url. Try again?
+{% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/templates/privacy.html b/templates/privacy.html
index 593d377..8cf1669 100644
--- a/templates/privacy.html
+++ b/templates/privacy.html
@@ -12,12 +12,12 @@
A unique post identifier
The post's time and date of publishing
Whether the post has any media attached
- Whether the post has been favourited by you
- (Mastodon only) Whether the post is a direct message
+ Whether the post has been favourited by you (only Twitter or Mastodon); or if (not how) you reacted to the post (Misskey only)
+ Whether the post is a direct message (only Mastodon or Misskey)
No other post metadata and no post contents are stored by Forget.
-Last updated on 2021-05-14. History.
+Last updated on 2021-11-11. History.
{% endblock %}